* fix(workflows): raise catalog error, not raw ValueError, on a malformed catalog URL
The four catalog URL validators in `workflows/catalog.py`
(`WorkflowCatalog`/`StepCatalog` `_validate_catalog_url`, and the nested
fetch-path validators) accessed `urlparse(url).hostname` unguarded. A
malformed authority — e.g. an unterminated IPv6 bracket `https://[::1`
or a bracketed non-IP host `https://[not-an-ip]` — makes urlparse /
hostname raise `ValueError`.
Each validator's contract is to raise a domain error
(`WorkflowValidationError` / `StepValidationError` /
`WorkflowCatalogError` / `StepCatalogError`), and the command handlers
catch only those. So `specify workflow catalog add "https://[::1"`
surfaced an uncaught `ValueError` traceback instead of the clean
`Error: Catalog URL is malformed` + exit 1 that a bad URL should give.
The fetch-path validators also run on the post-redirect `resp.geturl()`,
so a hostile redirect target could crash the fetch the same way.
Guard each `urlparse`/`.hostname` access with `try/except ValueError ->
domain error`, mirroring the fixes already applied to
`specify_cli.catalogs` (#3435) and the bundler adapters (#3433). Also
read `hostname` once and reuse it for the host check, matching those
siblings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(workflows): cover post-redirect malformed-URL guard (#3484 review)
Copilot review asked for regression tests on the fetch-path validators that
re-check resp.geturl() after redirects — the branch that turns a malformed
redirect target into a domain error instead of a raw ValueError.
- test_fetch_malformed_redirect_target_raises_catalog_error on both
TestWorkflowCatalog and TestStepCatalog: stub open_url with a response whose
geturl() is malformed (https://[::1 / https://[not-an-ip]/x) while entry.url
is valid, so validation only trips on the redirect target, and assert
_fetch_single_catalog raises WorkflowCatalogError / StepCatalogError with a
"malformed" message (force_refresh + fresh project_dir so no cache masks it).
- Test-the-test: both fail on pre-fix source (raw ValueError re-wrapped as
"...Invalid IPv6 URL", no "malformed" match) and pass with the guard.
Also merges latest upstream/main into the branch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): evaluate 'in'/'not in' safely on a non-iterable right operand (#3447)
The `in` / `not in` operators in `_evaluate_simple_expression` only guarded
`right is not None`, but `left in right` also raises `TypeError` for any other
non-iterable right operand (int, bool, float). So a workflow condition like
`{{ inputs.tag in inputs.count }}` where `count` is a number leaked a raw
`TypeError: argument of type 'int' is not iterable` and crashed the whole run,
instead of evaluating like the None case beside it.
This was asymmetric with `_safe_compare`, which already swallows `TypeError`
and returns False for the ordering operators.
Add a `_safe_contains` helper (mirroring `_safe_compare`) that treats both a
None and a non-container right operand as "nothing is contained": `in` -> False,
`not in` -> True. Add a regression test covering int/bool/float/None right
operands and asserting genuine containment against iterables still works.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(workflows): address review feedback on #3468#3447 was fixed independently by #3448 (merged first), which added the
same _safe_membership helper this branch introduced. Per Copilot review:
- Revert the redundant _safe_contains rename in expressions.py so the file
matches main; the working membership guard already lives there.
- Drop the duplicate test_in_operator_non_iterable_right_operand test and
fold its only new coverage (not in against float/bool/None right operands,
which the base test only checked for the int case) into the existing
test_membership_against_non_iterable_is_false_not_error.
Also merges latest upstream/main into the branch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
`save_init_options()` omitted a final newline, causing
`end-of-file-fixer` from .pre-commit-config.yaml (#3430) to flag
a diff on every `specify integration upgrade` run.
Append `\n` to the `json.dumps()` output to match POSIX
expectations and align with `integration_state.py` which already
includes the trailing newline.
Ref: https://github.com/github/spec-kit/pull/3430
* feat(workflows): align workflow CLI with extension command surface
Adds the missing workflow commands and flags so the workflow CLI
matches the extension/preset pattern: add --dev and --from, search
--author, update, enable and disable. Disabled workflows are blocked
from running and marked in list output.
Fixes#2342
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): preserve disabled state on update, guard corrupted registry entries
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): guard list against corrupted registry entries, re-raise typer.Exit in catalog install
workflow list now skips non-dict registry entries with a warning instead
of crashing, matching update/enable/disable. The broad except in
_install_workflow_from_catalog no longer swallows typer.Exit, so precise
errors like the non-HTTPS redirect message are not duplicated.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): escape rich markup in id-mismatch errors and validate --from source early
The two id-mismatch error paths interpolated repr() into Rich markup, so
a stray bracket in a user typo could be parsed as markup. Route both
through rich.markup.escape.
`workflow add <source> --from <url>` also validated the source only
after downloading. Validate it up front so a URL/path/typo fails
without a network fetch.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): escape rich markup in list output and catalog install errors, isolate update failures
workflow list now escapes id/name/version/description before printing,
matching how extensions render user-editable fields. The catalog install
helper computes safe_wf_id once and uses it for every early error path
plus the final failure message.
workflow update wraps _safe_workflow_id_dir and the backup read inside
the try/except typer.Exit block so an unsafe id in a corrupted registry
fails that one workflow and the rest continue.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): escape rich markup in --from download exception message
Matches how the catalog install path escapes exception strings.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): catch OSError in per-workflow update loop and make restore best-effort
Transient FS errors (perms, disk full) from backup read or write no
longer abort the whole update run. The restore is wrapped in its own
try/except so a failed write only warns, and the offending workflow
is reported via 'Failed to update' like other per-workflow failures.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): escape rich markup in search output
workflow search now escapes catalog-derived name/id/version/description/
tags before printing, matching extension search and workflow list.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: escape workflow validation errors before Rich output
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): escape remaining unescaped Rich markup paths
Covers the last few review threads not yet addressed:
- Escape yaml.YAMLError text in the local workflow add install path
(matches the already-escaped download/catalog paths).
- Escape the non---dev local directory fallback's "No workflow.yml
found in <path>" message (the --dev branch already escaped it).
- Escape the redirected final_url in the --from non-HTTPS redirect
error (IPv6 literals like http://[::1]/... are legal and contain
brackets).
- Escape the "Downloaded workflow is invalid" exception message in
_install_workflow_from_catalog, matching the sibling catalog-install
exception handler a few lines above it.
Adds regression tests for each in TestWorkflowCliAlignment, following
the existing escaping-test pattern in this class.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): escape workflow name/id in install success messages
Workflow names and ids come from user-controlled YAML or external catalog
data; printing them unescaped lets bracket characters be interpreted as
Rich tags. Escape them in the add/catalog-install success messages and the
remaining catalog error paths, matching the rest of the output hardening.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): fail cleanly on unparseable catalog install URLs
urlparse raises ValueError on e.g. an unbalanced IPv6 literal before the
invalid-URL branch is reached; on workflow update that also bypassed the
per-workflow handler and aborted the whole command. Convert the parse
failure into a clean error so add fails cleanly and update skips just the
affected workflow.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): reject catalog updates whose downloaded version mismatches
The update path never verified the downloaded workflow carries the catalog
version that triggered the update, so a stale or misconfigured URL could
report success while leaving the old version installed or downgrading it.
Pass the expected version into the install helper and fail the update when
the downloaded definition does not match.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): validate workflow ID in run command and document new CLI flags
Path-equivalent spellings like "align-wf/" previously bypassed the
registry disabled check because the engine normalizes the path while the
registry matches the raw string. workflow run now validates non-file
sources against the workflow ID pattern before lookup.
Also updates docs/reference/workflows.md with --dev/--from install
options, update/enable/disable commands, and the search --author flag.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): enforce disabled state for direct paths to installed workflows
Running the installed copy's YAML directly (specify workflow run
.specify/workflows/align-wf/workflow.yml) skipped the registry check.
File sources resolving inside .specify/workflows/<id>/ now map back to
the workflow ID and refuse to run while disabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): reject explicit empty --from URL instead of catalog fallback
'workflow add foo --from ""' fell through 'from_url or ...' to a
catalog install. Distinguish None from empty string so explicit values
stay on the URL-validation path and fail closed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): registry rollback on save failure, consistent disabled check, honest update summary
- WorkflowRegistry.add now rolls back its in-memory mutation when save()
raises, so a later successful save cannot persist metadata for a
failed update alongside the restored YAML backup.
- workflow run uses the same truthiness check for 'enabled' as list and
disable, so malformed values like 0 or null refuse to run.
- workflow update reports 'No workflows were eligible for update' when
every target was skipped instead of claiming all are up to date.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): guard non-string catalog URL and keep enable/disable rollback intact
- A truthy non-string catalog url (e.g. 123) reached urlparse and raised
AttributeError, escaping the clean error path; validate it is a string.
- enable/disable mutated the live registry entry before add(), so add's
rollback snapshot captured the already-toggled object; pass a fresh
mapping instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): tolerate non-dict registry entries in add and clarify test docstrings
A corrupted-but-parseable registry entry (e.g. a string value) crashed
WorkflowRegistry.add with AttributeError on existing.get. Guard the
non-dict case while still restoring the original raw value on rollback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): atomic registry save and accurate mixed-target update summary
- save() wrote the registry with open('w'), so a failed dump truncated
the file and the next load reset every entry. Write to a sibling temp
file and os.replace into place.
- workflow update no longer claims all workflows are up to date when
some targets were skipped; it reports checked-only status with a
skipped count.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): exclusive temp file for registry save and cwd-independent disabled guard
- save() now uses tempfile.mkstemp in the workflows dir (matching the
engine's atomic writer), so a pre-created symlink at a predictable
.tmp path cannot redirect the write and concurrent processes cannot
collide.
- The direct-path disabled guard derives the owning project from the
resolved file path instead of the caller's cwd, so running an
installed workflow's YAML from outside the project still refuses when
disabled.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): symlink guards and shape validation in workflow registry, dev-dir file check
- WorkflowRegistry now mirrors StepRegistry: _load refuses symlinked
parents/registry file and normalizes a non-dict workflows field;
save() rejects symlinked paths before writing.
- workflow add --dev requires workflow.yml to be a regular file so a
directory named workflow.yml gets the documented CLI error instead of
an uncaught IsADirectoryError.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): validate download redirects before following them
All three workflow download sites (add --from, catalog install, step
install) passed no redirect_validator to open_url, so an HTTPS URL
redirecting to cleartext HTTP issued the insecure request before the
post-hoc geturl() check reported it. Shared validator now rejects
non-HTTPS redirects (loopback HTTP allowed) pre-follow, matching the
preset download path.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(workflows): accept redirect_validator kwarg in step-add open_url fakes
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): guard directory-shaped workflow.yml and unreadable registry
- workflow add's plain local-path fallback (no --dev) checked wf_file.exists()
before installing, so a directory literally named workflow.yml passed the
guard and _validate_and_install_local() leaked an uncaught
IsADirectoryError instead of the documented CLI error. Use is_file(),
matching the --dev branch's existing guard.
- WorkflowRegistry._load() treated any OSError while reading an existing
registry the same as corrupted JSON, resetting to an empty in-memory
registry. A later save() would then silently persist that empty state via
os.replace, discarding every previously installed workflow entry. Track a
_load_error flag on OSError-during-read and have save() refuse to write
when it is set, so a transient I/O failure can no longer overwrite intact
data on disk.
- docs/reference/workflows.md: document `--from <url>` with its value
placeholder, matching extensions.md and presets.md.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflow): rollback registry.remove() and guard OSError at CLI boundaries
Critical: WorkflowRegistry.remove() deleted the in-memory entry then
called save() with no rollback, unlike add(). Combined with
workflow_remove deleting the workflow directory before calling
registry.remove(), a save failure permanently destroyed the workflow's
files, left the on-disk registry still claiming it installed, and
surfaced a raw unhandled OSError with no CLI message.
- WorkflowRegistry.remove() now rolls back the in-memory entry on a
save() OSError, mirroring add()'s existing rollback pattern.
- workflow_remove persists the registry removal (registry.remove(),
wrapped in try/except OSError -> clean escaped message) before
deleting any files, so a save failure never touches the workflow
directory.
Important sibling paths: workflow add (local/--dev/--from and catalog),
enable, and disable all called registry.add() without catching its
deliberate OSError, so a save failure surfaced either an orphaned
install directory (fresh local/catalog installs) or a raw/unhandled
exception with no clean CLI output.
- _validate_and_install_local (backs local/--dev/--from) now removes
the freshly created directory on a fresh install, or restores the
prior workflow.yml bytes on a reinstall-over-existing-local install,
before raising a clean escaped error.
- _install_workflow_from_catalog wraps the final registry.add() using
the function's own established convention (rmtree the just-downloaded
workflow_dir, then a clean escaped error) -- workflow_update's
existing backup/restore around this function is unaffected.
- workflow_enable/workflow_disable catch registry.add()'s OSError and
print a clean escaped message instead of leaking the exception.
Added failing-first tests proving each behavior (registry-unit rollback
test, CLI-level remove/add/enable/disable save-failure tests
parametrized where they share one root cause), all confirmed red before
the fix and green after.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflow): preserve prior catalog install on reinstall registry-save failure
_install_workflow_from_catalog's final registry.add() failure handler
unconditionally rmtree'd workflow_dir. That's safe for a brand-new
install, but plain `workflow add <catalog-id>` also allows re-adding an
already-installed workflow, downloading the new version over the
existing directory first. If registry.add() then failed to save, the
unconditional rmtree deleted the prior working install while the
registry (after its own rollback) still reported it installed -- data
loss with no way back. workflow_update already avoids this via an outer
backup/restore around this function, but plain add has no such caller.
Fix mirrors _validate_and_install_local's existed-before/backup-aware
handling: capture whether workflow_dir existed and back up its
workflow.yml bytes before any download write, then on a registry.add()
OSError, restore those bytes for a reinstall or rmtree only a
brand-new directory. Only one file (workflow.yml) is ever written by
this path, so no further per-file bookkeeping is needed.
Added a failing-first regression: install a catalog workflow, re-add it
with a simulated registry save OSError, and assert a clean error, the
original workflow.yml restored byte-for-byte, and the registry still
reporting the original version installed. Confirmed red (prior file
deleted) before the fix, green after.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflow): centralize catalog-install cleanup across all failure branches
_install_workflow_from_catalog is new in this PR and has seven failure
branches after the mkdir/download step, each independently rmtree'ing
workflow_dir: redirect-to-non-HTTPS rejection, a generic download
exception, invalid downloaded YAML, a validate_workflow failure, a
workflow-id/catalog-key mismatch, a version mismatch, and (fixed in the
prior commit) a registry.add() OSError. Only the last one had been
special-cased to spare a prior working install on reinstall; the other
six still unconditionally deleted the whole directory, so re-adding an
already-installed catalog workflow and hitting any of those six earlier
failures destroyed the working install even though nothing about it had
actually changed.
Replaced all seven ad hoc rmtree call sites with a single local
_cleanup_failed_install() helper that closes over the existed_before /
prior_workflow_bytes captured once at the top of the function: restore
the prior workflow.yml for a reinstall, or rmtree only a directory that
this attempt itself created. Every failure branch now calls this one
helper, so the fix is structural rather than duplicated, and every
existing error message/exit code is unchanged -- only the cleanup
performed before each message is different.
Added a parametrized regression test covering the four early-failure
trigger points reachable from plain workflow add (redirect rejection,
download exception, invalid YAML, ID mismatch): each installs a catalog
workflow, re-adds it while forcing that specific failure, and asserts a
clean error plus the original workflow.yml surviving byte-for-byte.
Confirmed red against the unfixed code (all four raised FileNotFoundError
reading the deleted file) before applying the helper, green after.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflow): restore registry entry verbatim on post-removal rmtree failure
workflow_remove now persists registry.remove() before deleting any
files (fixed previously), but if the registry write succeeds and the
subsequent shutil.rmtree(workflow_dir) then fails, the registry was
left claiming the workflow uninstalled while its directory remained on
disk -- an orphaned install with no path back to a clean state.
workflow_step_remove already handles this exact sequencing by capturing
the registry entry before removal and restoring it directly into
registry.data plus save() (bypassing add(), which would stamp a new
updated_at) if the directory removal fails afterwards.
Applied the same pattern to workflow_remove: capture registry_metadata
via registry.get() before registry.remove(), and on an rmtree OSError,
write it straight back into registry.data["workflows"][workflow_id] and
save(), matching workflow_step_remove's restore-failure handling (a
yellow warning, not a hard failure, since the primary error is already
about to be reported). Existing error message and exit behavior for the
rmtree failure are unchanged.
Added a failing-first regression: install a workflow, monkeypatch
shutil.rmtree to raise OSError, and assert a clean existing error
message, the directory remaining (rmtree never actually deleted
anything), and the registry entry restored byte-for-byte identical
(including installed_at/updated_at) -- proving the fix bypasses add()
and doesn't re-stamp timestamps. Confirmed red (registry entry stayed
None) before the fix, green after.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix 4 current Copilot review findings on workflow run/registry/install
1. workflow run ownership check followed symlinks via Path.resolve()
before mapping a direct YAML path back to its installed workflow ID.
A symlinked .specify/workflows/<id>/workflow.yml resolved outside the
tree, missed the ownership match entirely, and let the disabled-workflow
guard be silently skipped while engine.load_workflow still followed the
symlink. Now maps ownership from a lexically-normalized path (os.path.
normpath, no symlink following) and explicitly refuses to run if the
installed <id> directory or workflow.yml leaf is itself a symlink.
Direct external workflow paths that don't match .specify/workflows/...
are unaffected.
2. WorkflowRegistry._load() caught a read OSError and silently fell back
to an empty in-memory registry, only blocking a later save(). Callers
that only query is_installed()/get()/list() before writing a file
(e.g. commands/init.py's bundled speckit install, which overwrites
workflow.yml once is_installed() reports false) could act on that
false-empty state and destroy real data before ever reaching save().
_load() now raises OSError immediately so an unreadable registry fails
closed at construction, before any query or side effect is possible.
Added _open_workflow_registry() to give every CLI command a consistent
clean-error boundary around registry construction.
3. _validate_and_install_local's mkdir/copy2 ran before the try/except
that protected registry.add(); a copy2 failure (e.g. a truncating
partial write on a reinstall) was not caught at all, so the existing
backup-restore cleanup never ran and the prior working workflow.yml
was corrupted with a raw traceback surfaced to the user. mkdir/copy2
now run inside the same rollback-protected section as registry.add(),
sharing one _cleanup_failed_install() helper.
4. workflow update's skip message claimed any non-catalog source was
installed "from a local path or URL", which is wrong for the bundled
speckit workflow (source: "bundled"). Message is now source-neutral.
Verified all 4 threads are current (not outdated) via GraphQL review
thread query on PR #3419, HEAD 812050a.
Tests: strict TDD per fix (red test proving each bug, minimal production
change, green). tests/test_workflows.py: 474 passed. Full suite: 3976
passed, 110 skipped. ruff check: all checks passed on touched files and
full src tree.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix disabled-workflow bypass via symlinked .specify project root
workflow run's ownership check derived registry_root/registered_id from
the lexical path, then checked the id directory and workflow.yml leaf for
symlinks -- but never checked .specify or .specify/workflows themselves
for that derived root. _reject_unsafe_workflow_storage only guards the
cwd's project_root, which can differ from the path-derived registry_root
(a direct path into an unrelated project, or that project's own .specify
being a symlink to an attacker-controlled tree). WorkflowRegistry's own
symlinked-parent handling silently substitutes an empty registry instead
of raising, so a query against it (is_installed/get returning "not
found") is not a safety signal a caller can rely on: with a symlinked
.specify, the disabled check saw no registry entry and let a disabled
workflow run anyway.
Fix: reject an unsafe .specify/.specify-workflows for the actual derived
registry_root before ever consulting the registry, reusing the existing
_reject_unsafe_dir helper already used by _reject_unsafe_workflow_storage.
Red-first end-to-end repro: victim project's .specify symlinked to an
attacker-controlled tree containing a disabled workflow entry, run
invoked with a direct path from an unrelated cwd -- confirmed the
disabled workflow executed (exit 0) before the fix, now refused cleanly.
Tests: tests/test_workflows.py 475 passed. Full suite: 3977 passed, 110
skipped. ruff check: all checks passed.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix raw exception leak in bundle remove primitive boundary
remove_bundle() had no exception handling around its component
removal loop, unlike install_bundle() which converts any raw
exception into a clean BundlerError. Since WorkflowRegistry now
fails closed (raises OSError) on an unreadable registry file,
and _WorkflowKindManager.__init__ constructs WorkflowRegistry
with no try/except, an unreadable workflow registry surfaced as
a raw OSError through remove_bundle(). The bundle_remove CLI
command only catches BundlerError, so the raw OSError propagated
uncaught, producing exit_code=1 with empty output instead of a
clean, actionable message.
Wrap remove_bundle()'s component loop in the same
try/except BundlerError: raise / except Exception: raise
BundlerError(...) from exc pattern already used by
install_bundle(), converting any raw exception at this shared
boundary. save_records() remains outside the try block, so a
failure still leaves the bundle's record untouched (no removal
side effects recorded).
Tests:
- tests/integration/test_bundler_install_flow.py::test_remove_converts_raw_installer_exception_to_bundler_error
(function-level regression: a raw OSError from installer.is_installed
must become a clean BundlerError, and the bundle record must survive)
- tests/contract/test_bundle_cli.py::test_remove_reports_clean_error_when_primitive_raises_raw_exception
(CLI-level regression: `specify bundle remove` must print a clean
actionable message and exit non-zero instead of raw/empty output)
Both tests were confirmed red beforehand: the raw OSError propagated
uncaught out of remove_bundle(), and the CLI-level CliRunner result
showed exit_code=1 with empty output.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix 8 current Copilot review findings on registry fail-closed, rollback orphans, backup-read boundaries, and Rich escaping
1. WorkflowRegistry._load(): a symlinked .specify/.specify/workflows
parent (or a symlinked registry file) silently returned an empty
registry instead of raising, unlike an unreadable-file read failure.
A read-only caller (notably the bundler's remove path) querying
is_installed() before ever writing could conclude an installed
workflow is absent, skip removing it, then delete the bundle
record -- leaving the workflow untracked but still on disk. Now
raises OSError immediately, matching the existing unreadable-file
fail-closed behavior.
2/8. _validate_and_install_local and _install_workflow_from_catalog:
when the destination directory already existed but had no prior
workflow.yml (e.g. a leftover empty dir), existed_before was True
but there were no backup bytes to restore, so the rollback closure
did nothing on a later failure -- leaving the newly copied/
downloaded file behind. Both now unlink the newly created file in
this case, restoring the pre-existing directory to its prior
(empty) state.
3/4. Both install paths read the prior workflow.yml bytes (to seed
the reinstall rollback) *before* any try/except boundary: a read
failure on the existing file (e.g. a transient permission/FS
issue) leaked a raw, unescaped OSError instead of the same clean
CLI error used by every other failure branch in these functions.
Both reads are now guarded by their own try/except OSError, with
no writes attempted before the read succeeds (so there is nothing
to roll back on this specific failure).
5. remove_bundle's exception-conversion message unconditionally
claimed "No changes were recorded," even though a failure can
occur after earlier components in the same bundle have already
been removed from disk (save_records never runs on this path, so
the record is left claiming the bundle fully installed). The
message now reports how many components were already removed
when that happened, instead of asserting no changes occurred.
6/7. workflow_remove's new post-registry-removal directory-failure
error and its restore-failure warning interpolated workflow_dir
and the exception values into Rich markup unescaped. A project
path or OS error message containing Rich-markup-like brackets
could be parsed as markup and hide/corrupt the displayed text.
Both now use the existing _escape_markup helper, consistent with
every other error path in this file.
Tests (tests/test_workflows.py unless noted):
- TestWorkflowRegistry::test_load_symlinked_workflows_dir_fails_closed_not_silently_empty (1)
- TestWorkflowCliAlignment::test_add_dev_fresh_install_into_preexisting_empty_dir_cleans_new_file (2)
- TestWorkflowCliAlignment::test_add_catalog_fresh_install_into_preexisting_empty_dir_cleans_new_file (8)
- TestWorkflowCliAlignment::test_add_dev_reinstall_backup_read_failure_gives_clean_error (3)
- TestWorkflowCliAlignment::test_add_catalog_reinstall_backup_read_failure_gives_clean_error (4)
- tests/integration/test_bundler_install_flow.py::test_remove_partial_failure_message_reflects_partial_state (5)
- TestWorkflowRemoveGuard::test_remove_directory_and_restore_failure_escapes_rich_markup (6/7)
All seven were confirmed red beforehand, matching each thread's
described failure mode exactly (silent empty registry instead of a
raise; orphaned new file left behind; raw unescaped OSError leaking;
a misleading "no changes were recorded" claim; Rich markup consuming
bracketed path/exception text). Also updated
test_registry_save_refuses_symlinked_parent, a pre-existing test that
asserted the symlinked-parent raise at add()/save() time -- it now
raises at construction instead, per fix#1, so the test was adjusted
to match without weakening its guarantee (still asserts no writes
occur under the symlinked target).
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix 3 current Copilot review findings: bookkeeping-aware BundlerError removal, bounded workflow downloads
1. bundle remove: BundlerError raised by the primitive installer itself
(e.g. from a kind manager) bypassed the partial-removal bookkeeping
message added previously via a bare `except BundlerError: raise`. Now
routes through the same detail-construction logic as generic
exceptions, so a mid-loop BundlerError after an earlier successful
removal still reports that the project may be partially uninstalled,
while a zero-removal BundlerError still reports "No components were
removed." Both preserve the original exception message and chain
`from exc`.
2/3. workflow add --from and catalog install/update downloads used
unbounded `response.read()`, buffering the entire server-controlled
body into memory before any size check, and trusted Content-Length
alone where checked at all. Added a single shared
`_read_response_within_limit()` helper reused by both call sites: it
fails fast on an oversized declared Content-Length, and separately
enforces the same cap while streaming in 64KiB chunks so a chunked or
Content-Length-less response cannot bypass the limit by lying about or
omitting its size. Chose 5 MiB as the cap: workflow YAML definitions
are small step/metadata text, not binaries, so this is generous
headroom against a malicious/misbehaving server without affecting any
legitimate workflow definition. Both call sites already route any
raised exception through their existing clean-error and rollback
(`_cleanup_failed_install`) paths, so no additional error-handling
plumbing was needed.
Tests: extended the shared `_FakeResponse` test helper (and 5 duplicate
per-test FakeResponse classes) to support `.read(amt)` chunked reads with
an internal cursor (backward compatible with existing bare `.read()`
callers) plus header simulation. Added red-first tests for: BundlerError
after partial removal reporting partial state, BundlerError with zero
removals reporting no changes, --from oversized-Content-Length rejection,
--from oversized-streamed-body-without-Content-Length rejection, and the
same two cases for the catalog install path (asserting no orphan
directory/registry mutation on rejection).
tests/integration/test_bundler_install_flow.py: 17 passed
tests/test_workflows.py: 485 passed
tests -q: 3992 passed, 110 skipped
ruff check: clean on all touched files
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix temp-file leak in workflow add --from and strengthen size-limit test assertions
workflow_add's --from download path opened a NamedTemporaryFile(delete=False)
-- which creates the file on disk immediately -- then wrote the size-limited
response body before assigning `tmp_path`. If `_read_response_within_limit`
raised (oversized declared Content-Length, or an over-cap streamed body with
no/understated Content-Length), the exception propagated out of the `with`
block before `tmp_path` was ever set, so the outer except handler had no
path to clean up: a 0-byte `.yml` temp file was left behind permanently on
every rejected/failed --from download. Fixed by assigning `tmp_path`
immediately after the file is opened (before the size-limited read/write),
and unlinking it in the except branch when set. Normal post-download cleanup
in the existing `finally: tmp_path.unlink(missing_ok=True)` is unchanged.
Verified (not assumed) the catalog install path has no equivalent leak: it
writes the response bytes directly to `workflow_file` inside `workflow_dir`
(no separate temp file), and any read/size-limit failure is already caught
by the existing `except Exception: _cleanup_failed_install()` handler, which
correctly restores a reinstalled file or removes a freshly-created directory.
While investigating, found the previous round's 4 size-limit tests were
false positives: `_read_response_within_limit`'s `max_bytes` parameter had
its default bound to `_MAX_WORKFLOW_YAML_BYTES` at function-definition time,
so monkeypatching the module attribute in tests had no effect on the
function's actual behavior -- the tests were passing because the oversized
mock bodies failed downstream YAML/id validation instead of the size check.
Fixed by resolving `max_bytes` from the module attribute at call time
(default `None`, resolved inside the function body) so tests can actually
override the effective limit, and strengthened all 4 tests' assertions to
match the specific size-limit error text (whitespace-collapsed to tolerate
Rich's line-wrapping), so they now prove the real code path fires.
Tests: added 2 red-first regression tests (oversized-streamed-body and
oversized-Content-Length --from downloads leave no leftover temp file,
verified against a scratch tempfile.tempdir), confirmed red (real 0-byte
file found) before the fix and green after. Strengthened the pre-existing
4 --from/catalog size-limit tests to assert on the actual error message
instead of generic exit-code/non-empty-output checks.
tests/test_workflows.py: 487 passed
tests -k bundler: 186 passed
tests -q: 3994 passed, 110 skipped
ruff check: clean on all touched files
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Harden workflow install/remove transactions with atomic staging
Addresses 5 Copilot review findings on HEAD b8269c8, all centered on
transaction integrity around workflow install/remove/registry writes,
following the atomic_write_json pattern already used in _utils.py:
1. WorkflowRegistry.save() now preserves the existing registry file's
mode (e.g. 0640/0644) across a save instead of silently downgrading
it to mkstemp's 0600 default; a brand-new registry still gets the
secure 0600 default.
2. workflow_remove now stages the install directory out of the way via
an atomic rename *before* the registry write, rather than deleting
it directly with shutil.rmtree after the registry already claims it
removed. This closes a real data-integrity gap: a partially-failed
rmtree could no longer leave a damaged directory re-marked
"installed" by the old manual restore-after-rmtree-failure code
(now deleted -- it's structurally impossible to need it). A
registry-write failure renames the staged directory back
(guarded, with an explicit warning if the restore-back rename
itself fails); a registry-write success is durable, so a later
failure to delete the staged directory is now a warning (exit 0),
not a contradictory "Error: Failed to remove" (exit 1) that used to
claim failure while the registry already recorded success.
3. Local (--dev/--from/plain path) and catalog install/reinstall now
write new content to a same-directory staging file and commit it
onto the destination workflow.yml via a single atomic swap, instead
of writing/downloading directly into the destination file. A prior
file (reinstall) is renamed aside rather than overwritten in place,
so it can be restored via rename -- never a content rewrite -- if
registry.add() subsequently fails; a rollback failure is now
explicitly reported as a warning instead of escaping unguarded and
masking the original clean error. This also removes the need to
read the prior file's bytes into memory before installing (that
read-before-write step and its failure mode are now unreachable),
and both local and catalog installs share the same four small
helpers (_stage_workflow_file / _commit_workflow_file /
_discard_staged_workflow_file / _rollback_committed_workflow_file,
plus guarded wrappers) rather than duplicating the logic.
4. Updated a stale comment (workflow_run's ownership-guard rationale)
that still described WorkflowRegistry._load() as silently
substituting an empty registry; it now fails closed by raising
OSError, which the comment now states plainly.
Tests: rewrote the two workflow_remove tests whose assertions encoded
the old (incoherent) rmtree-then-restore contract to instead prove the
new stage-then-commit contract (post-registry-success cleanup failure
is a warning+exit 0; pre-registry-success stage-restore failure is
guarded and escapes markup correctly). Rewrote the local/catalog
"backup read failure" tests, which tested a step the new design no
longer performs, into "restore-rename failure" tests proving the new
guarded rollback boundary. Added registry file-mode preservation tests.
All other existing install/remove/reinstall tests (save-failure
cleanup, pre-existing-empty-dir handling, early-failure-during-
reinstall parametrized cases, Rich markup escaping) continue to pass
unmodified against the new implementation.
Verified via GraphQL that all 5 threads are current (not outdated/
resolved) before fixing. Full suite: 3996 passed, 110 skipped. Ruff
clean on all touched files.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Discard reinstall backup file after registry.add() succeeds
_commit_workflow_file() renames a prior workflow.yml aside to
workflow.yml.bak so it can be restored if registry.add() subsequently
fails. Neither the local install/reinstall path nor the catalog
install/reinstall path ever cleaned up that backup after a successful
registry.add() -- every successful reinstall permanently left a
workflow.yml.bak sibling, which later reinstalls would silently
overwrite/re-orphan.
Add a shared _discard_committed_backup_file() helper, called from both
success paths right after registry.add() durably succeeds (and before
the final "installed" message, preserving output ordering). A fresh
install (backup_file is None) is a no-op. A cleanup failure is reported
as a warning (exit 0), not a failure, since the install itself already
succeeded -- consistent with workflow_remove's post-commit cleanup
warning semantics.
Add red-first regression tests proving: (1) successful local reinstall
leaves no workflow.yml.bak sibling, (2) successful catalog reinstall
leaves no workflow.yml.bak sibling, (3) a cleanup failure on the backup
file after a successful reinstall reports a warning and still exits 0
with the registry correctly reflecting the new install.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Clean up freshly-created dest_dir when staging mkstemp fails
_stage_workflow_file() does dest_dir.mkdir(parents=True, exist_ok=True)
then tempfile.mkstemp(dir=dest_dir, ...). For a fresh install (no prior
directory), if mkdir succeeds but mkstemp then raises (disk
full/EMFILE/quota), the exception previously propagated straight past
both the local-install and catalog-install call sites without any
cleanup, leaving the newly-created empty workflow directory orphaned
on disk with no error indicating why.
Fix at the shared _stage_workflow_file() boundary instead of duplicating
cleanup at each call site: track whether this call created dest_dir: on
a mkstemp failure, remove that directory via a guarded rmdir (never a
broad rmtree, so any concurrently written content would be left
untouched) before re-raising the original OSError unchanged. A
pre-existing (reinstall) dest_dir is never touched by this cleanup,
and a cleanup failure is reported as its own warning without masking
the original error.
Add red-first regression tests proving: a fresh local install (--dev,
plain local path, --from) and a fresh catalog install both clean up the
orphaned directory on a simulated mkstemp failure, and a reinstall over
a pre-existing directory is left untouched by the same failure.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix installed-workflow ownership/disabled bypass and resume enforcement
Address 3 current Copilot review findings on the disabled-workflow guard
in `workflow run`/`workflow resume`:
- The lexical `.specify/workflows/<id>` ownership scan stopped at the
first match scanning from the start of the path. A nested project
living beneath an outer installed workflow's own directory tree (reusing
the same segment names) was attributed to the wrong (outer) workflow
and ID, gating the run on an unrelated workflow's disabled state.
`_scan_for_workflow_owner` now scans from the end so the nearest
(innermost) owner always wins.
- A path with no `.specify/workflows` segments of its own (e.g.
`/tmp/alias.yml`) that is itself a symlink resolving *into* installed
storage bypassed the disabled check entirely, since only the raw
lexical path was inspected. `_resolve_installed_workflow_ownership` now
additionally resolves the real path when the lexical scan finds no
owner and re-runs the same scan against it, so an outward-pointing
alias into a disabled workflow is caught too. Genuinely standalone
external files (no symlink anywhere on the path) are unaffected.
- `workflow resume` bypassed the disabled check altogether: engine.resume()
replays a persisted run directly from disk with no registry awareness.
RunState now optionally persists `installed_workflow_id` and
`installed_registry_root` at run start (set by workflow_run when the
source resolved to an installed ID); `workflow_resume` pre-loads the
run state and re-checks the registry's *current* disabled state before
calling engine.resume(), mirroring workflow_run's own guard. Both new
fields default to None via RunState.load()'s `.get()`, so runs from a
direct/non-installed source, and any run persisted before this schema
addition, resume exactly as before.
The ownership-mapping logic (previously inlined in workflow_run) is
extracted into `_resolve_installed_workflow_ownership` /
`_scan_for_workflow_owner` so both the lexical and resolved-path cases
share the same scan and the existing inward-symlink-component refusal.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Guard --from temp cleanup; drop redundant update rollback; mark POSIX-only tests
Two more current Copilot review findings, both in workflow_add/update:
- `workflow add --from`'s `finally: tmp_path.unlink(missing_ok=True)` ran
unguarded after `_validate_and_install_local` had already committed the
file and registry entry (success) or already raised its own clean
`typer.Exit` (failure). An OSError from that cleanup unlink would
surface as an unhandled failure even though the install itself
succeeded. It is now wrapped in try/except OSError, printing a neutral
warning that doesn't claim success or failure (the finally runs on both
outcomes) instead of propagating.
- `workflow_update`'s per-item loop performed its own outer backup
(`wf_file.read_bytes()`) and restore (`wf_file.write_bytes(backup)`)
around `_install_workflow_from_catalog`, which is itself fully
transactional (staged download, atomic rename-based commit, its own
rollback on registry failure) and never leaves a raw OSError or a
partially-written workflow.yml. The outer restore was therefore dead
weight for its stated purpose, and — being an unguarded byte-level
write — was itself an unnecessary place a second failure could truncate
an already-safely-preserved file. Removed; the loop now only records
success/failure.
Also marks 3 registry-save file-mode tests
(`test_registry_save_preserves_existing_file_mode`,
`test_registry_save_on_new_registry_uses_secure_default_mode`,
`test_registry_save_failure_preserves_file_on_disk`) as POSIX-only via
the repo's existing `skipif(sys.platform == "win32", ...)` pattern, since
they assert exact POSIX permission bits that don't hold on Windows.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Report possible partial changes on zero-removed bundle removal failure
The final Copilot review finding: `remove_bundle`'s zero-removed-components
error message claimed "No components were removed." even when the failing
installer component may have deleted files before raising -- prior review
rounds already established that DefaultPrimitiveInstaller's removal paths
are not atomic and can leave partial filesystem changes despite raising
before `result.uninstalled` is populated. The zero-count message is now a
conservative caution ("...but the failing component may have made partial
changes before raising, so the project may be partially uninstalled.")
instead of an unconditional claim of no side effects. The >0-removed path
(which already reports the confirmed partial list) is unchanged.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix workflow resume disabled-check bypass after project move/rename
RunState.installed_registry_root previously persisted the creation-time
absolute project path unconditionally whenever a run belonged to an
installed workflow. After the whole project directory was renamed or
moved, workflow_resume would open a WorkflowRegistry at that now
nonexistent path, get back an empty/default registry, and silently skip
the disabled-workflow check -- a paused run for a disabled workflow could
be resumed successfully from the new location.
Fix persists installed_registry_root only when the owning root genuinely
differs from the current project_root (true cross-project direct-file-
source invocations). The common same-project case now persists None and
is re-derived from the live project_root at resume time via a new
_resolve_run_owner_root() helper, which also falls back to project_root
if a stored root no longer exists on disk -- covering both the common
case transparently surviving project moves and the cross-project case
degrading safely if its owner project vanishes, rather than silently
skipping the disabled check.
Backward compatible: state files missing the new fields, and states with
a still-existing distinct cross-project root, behave unchanged.
Added regression tests:
- resume blocked after project moved then disabled at new location
- resume still works after project moved while workflow stays enabled
- cross-project registry root is still correctly honored when it exists
- resume falls back to current project's registry when a stored
cross-project root no longer exists
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix silenced cleanup failures and malformed run-state type validation
Four fixes from Copilot review on HEAD 4f24735:
1. _discard_staged_workflow_file's fresh-install directory removal used
shutil.rmtree(dest_dir, ignore_errors=True), so a genuine cleanup
failure there could never reach _safe_discard_staged_workflow_file's
warning -- an orphaned directory was left behind with zero report.
Now removes only if dest_dir still exists and lets a real OSError
propagate to the existing safe wrapper, which warns while the
already-printed original install error remains primary.
2. _rollback_committed_workflow_file's fresh-install directory removal
(post registry.add() failure) had the same ignore_errors=True gap;
fixed identically so _safe_rollback_committed_workflow_file's warning
can actually fire.
3. In the --from download-failure branch, tmp_path.unlink(missing_ok=
True) was unguarded: if it raised (e.g. read-only tempdir), it
replaced the original "Failed to download workflow" error with a raw
unhandled OSError instead of a clean typer.Exit. Now guarded exactly
like the later post-install finally cleanup: a cleanup failure prints
a warning and the original download error is still reported cleanly.
4. RunState.load() trusted installed_workflow_id/installed_registry_root
straight out of state.json with no type validation. A malformed value
(int/list/dict/bool instead of str-or-null) would crash deep inside
_resolve_run_owner_root or the registry lookup (TypeError building a
Path, unhashable dict/list as a mapping key) instead of failing
cleanly. Both fields are now validated as str | None during load,
raising a clear ValueError that workflow_resume's existing ValueError
boundary already converts into a clean CLI error with no traceback.
Valid values (including the empty-string fallback already handled by
_resolve_run_owner_root) continue to load unchanged.
Added red-first regression tests for each: staged-discard cleanup
warning, rollback cleanup warning, download-failure cleanup-vs-original-
error precedence, and parameterized malformed/valid run-state field
coverage.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add ValueError boundary to workflow status single-run lookup
QUALITY re-review flagged that fa410d3's new RunState.load() type
validation (malformed installed_workflow_id/installed_registry_root
raising ValueError) leaked as a raw unhandled traceback through
`workflow status <run_id>`, which only caught FileNotFoundError.
`workflow resume` already had the matching ValueError boundary.
Adds an `except ValueError as exc: console.print(f"[red]Error:[/red]
{exc}"); raise typer.Exit(1)` clause mirroring resume's exact pattern
(unescaped interpolation, consistent with the existing convention at
every other ValueError boundary in this file). FileNotFoundError
behavior and the no-run-id list-all-runs path are unchanged.
Added parametrized regression covering malformed installed_workflow_id/
installed_registry_root (int/list) via `workflow status`, plus
regressions locking in the unaffected FileNotFoundError and no-run-id
list-path behaviors.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: bound workflow step downloads
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: preserve workflow reinstall state
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: fail closed on workflow registry state
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: make workflow installs transactional
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: close workflow transaction races
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: clean up failed workflow transactions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: clean up workflow removal state
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: guard workflow update transactions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: harden workflow lifecycle edge cases
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: verify installed workflow ownership
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: isolate workflow rollback cleanup
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: preserve unique workflow backups
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: bind workflow staging to file descriptors
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: fail closed on corrupt workflow registry
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: bind workflow ownership and source identity
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): harden redirects and Windows tests
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): restore staged removals on serialization errors
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): preserve state across interrupted writes
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): harden resume ownership checks
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): validate persisted run state
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): validate origin and release metadata
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(extensions): stop env-var config leaking across prefix-colliding IDs (#3494)
Because ``_`` doubles as both the separator between an extension ID and
its config path AND the substitute for ``-`` inside an extension ID, an
env var like ``SPECKIT_GIT_HOOKS_URL`` starts with *both* the
``SPECKIT_GIT_`` prefix of the ``git`` extension and the
``SPECKIT_GIT_HOOKS_`` prefix of a co-installed ``git-hooks`` extension.
``ConfigManager._get_env_config`` matched only on the shorter prefix,
so the same env var silently surfaced inside both extensions' configs
(as ``{'hooks': {'url': ...}}`` for ``git`` and ``{'url': ...}`` for
``git-hooks``).
Impact: config intended for one extension leaked into another and, worse,
could flip ``config.<field> is set`` hook conditions on the wrong
extension.
Route the env var to the extension whose normalized ID is the longest
match — the more specific one. When another installed sibling's
normalized ID + ``_`` claims the remainder, skip the var here. The
sibling scan reads ``.specify/extensions/`` directly and degrades to a
no-op if the dir is missing (fresh project / ad-hoc harness), so the
pre-fix single-extension behaviour is unchanged when there is no
collision.
Distinct from #3350 (intra-extension prefix collision between two keys
of the same extension) — this fixes the cross-extension case.
Fixes#3494
* fix(extensions): source sibling scan from registry, not directory
Address Copilot review on #3497: ``ExtensionManager.remove(...,
keep_config=True)`` preserves the extension directory but drops the
registry entry, so the previous directory-scan approach would treat a
config-only leftover as an installed sibling and silently discard
``SPECKIT_<sibling>_*`` env vars into no owner. Sourced the sibling
list from ``ExtensionRegistry.keys()`` — the registry is the source of
truth for "installed" — and kept the same graceful ``[]`` fallback so
the fresh-project / ad-hoc harness path is unaffected. Updated the
``TestConfigManagerCrossExtensionEnvLeak`` ``_install`` helper to
register its fake installations and added
``test_config_only_leftover_not_treated_as_sibling`` to lock in the
new behaviour for the ``keep_config=True`` scenario.
Full suite: 3978 passed, 110 skipped.
* fix(extensions): swallow non-UTF-8 registry in sibling scan
Address Copilot follow-up on #3497: ``ExtensionRegistry._load()`` catches
``JSONDecodeError`` / ``FileNotFoundError`` but not decode failures — a
registry file with invalid text encoding would surface a
``UnicodeDecodeError`` out of ``_sibling_extension_ids`` and break every
config read instead of degrading to the documented pre-fix behaviour.
Extend the fallback in ``_sibling_extension_ids`` to also catch
``UnicodeError`` and add ``test_non_utf8_registry_does_not_crash`` as a
regression pin (kept ``_load()`` itself out of scope — that broader
hardening belongs in a separate PR since it affects all readers).
Full suite: 3979 passed, 110 skipped.
* fix(integrations): escape control characters in goose recipe YAML renderer
YAML forbids C0 control characters (except tab and newline) and DEL in
every scalar form, and a bare CR acts as a line break inside a block
scalar. _render_yaml wrote the body verbatim into a |2 literal block
scalar, so such bodies produced recipes the YAML parser rejects. Detect
block-scalar-unsafe characters and fall back to an escaped double-quoted
scalar via yaml.safe_dump, mirroring the TOML renderer's fallback
strategy from #3341.
Fixes#3382
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(integrations): use sys.maxsize instead of float inf for yaml width
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(integrations): extend block-scalar guard to C1 controls and Unicode line breaks
YAML's printable set excludes C1 controls (U+0080-U+009F except NEL), and
YAML 1.1 treats NEL/LS/PS as line breaks inside a literal block scalar, so
bodies carrying any of these still produced unparseable recipes. Widen the
fallback guard to the full class and cover it in the regression loop.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(integrations): also treat surrogates and U+FFFE/U+FFFF as block-scalar unsafe
YAML's printable set also excludes lone UTF-16 surrogates and the
non-characters U+FFFE/U+FFFF; bodies carrying them still hit the literal
block path and produced unparseable recipes. Extend the guard and the
regression loop.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(integrations): clarify YAML prompt serialization
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Multi-Repo Branch Sync extension to community catalog
Add multi-repo-sync extension submitted by @sebastienthibaud to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table
Closes#3406
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: restore multi-repo-sync sha256
* fix: update multi-repo-sync link text and catalog updated_at
- Change extensions.md link text from 'spec-kit-multi-repo-sync' to 'multi-repo-sync' to match extension ID convention
- Refresh catalog.community.json top-level updated_at to 2026-07-13T00:00:00Z
Assisted-by: GitHub Copilot (model: claude-sonnet-4.5, autonomous)
* fix: update multi-repo-sync entry timestamps to 2026-07-13
Set created_at and updated_at to 2026-07-13T00:00:00Z to match the
catalog publication date, per add-community-extension/SKILL.md:86-87.
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* chore: bump version to 0.12.14
* chore: begin 0.12.15.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(workflows): validate command step input/options are mappings
CommandStep.validate() only checked for 'command'; execute() then does input.items() and options.update(step_options). A non-mapping input:/options: (e.g. a YAML list or scalar) raised AttributeError at run time, bypassing the per-step FAILED/continue-on-error contract -- unlike the sibling steps (switch 'cases', fan-out 'step') which type-check their config fields in validate(). Add the same checks, plus a defense-in-depth coercion in execute() since the engine does not auto-validate before running a step.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: fix code-comment typo in CommandStep.validate
The explanatory comment said options.update(options) but execute() does
options.update(step_options). Comment-only change; no behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(workflows): command step FAILS on malformed input/options instead of coercing
execute() previously coerced a non-mapping 'input' to {} and silently ignored a
non-mapping 'options', then dispatched the command anyway. For a workflow that
skipped validation (the engine does not auto-validate before execute()), that
let an explicitly malformed step run with empty args and report COMPLETED —
masking the config error and defeating the per-step FAILED / continue_on_error
semantics this change is meant to provide.
Both now return a FAILED StepResult with the same contract error validate()
reports (never crashing on .items()/.update()). Valid mapping configs are
unaffected. Strengthened the execute() test to assert FAILED + the exact
'must be a mapping' error for input and options (fails before: the result
carried the downstream dispatch error, not the shape error).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(presets): resolve() honors manifest-declared file: for installed presets
PresetResolver.resolve()'s tier-2 (installed presets) loop was
convention-only: it looked for templates/<name>.md and <name>.md,
ignoring a preset manifest that declares the template with an explicit,
non-convention file: path. So resolve() returned the core template (and
resolve_with_source() misattributed source='core') while
collect_all_layers()/resolve_content() correctly used the preset's
declared file — a divergence inside the same class. It could also return
a stray convention-path file the manifest deliberately points away from.
Mirror collect_all_layers()'s manifest-first logic: use the declared
file: when present (skip convention fallback if it's missing, to avoid
masking typos), and fall back to the convention walk only when the
manifest is absent or doesn't list the template.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(presets): clarify the empty/falsey manifest-file branch comment
Per review: 'file' is a required key for every template entry
(PresetManifest._validate()), so the manifest-found branch is reached
for an empty/falsey/non-usable 'file' value, not a truly absent one.
Reword the comment to say so. Comment-only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(presets): resolve() returns only real files; test missing-file skip
Per review:
- Use is_file() (not exists()) when honoring a manifest-declared file: so a
manifest pointing at a directory is treated as missing rather than
returned to a caller that will read_text() it. Applied in both resolve()
and collect_all_layers() so the two stay consistent.
- Add a regression test for the skip-convention-fallback-when-declared-file-
missing behavior: manifest declares a missing custom/spec.md while the pack
has a convention templates/spec-template.md; resolve() must skip the pack
and fall through to core, not pick up the stray convention file.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(presets): resolve()/collect_all_layers() require a regular file for manifest file:
A manifest-declared file: path is honored via exists(), which also accepts
a directory. If a preset points file: at a directory, resolve() returned it
and downstream read_text() crashes. Use is_file() in both resolve() and
collect_all_layers() so a non-file (directory) is treated as missing and the
convention fallback is skipped (pack yields to core), matching the existing
missing-file behavior.
Adds a directory-at-file: test (fails on exists(), passes on is_file()) that
also asserts collect_all_layers() never returns the directory as a layer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(presets): extract shared _manifest_declared_template for resolve()/collect_all_layers()
Both methods reimplemented the manifest-entry lookup + authoritative-fallback
rules independently — the exact duplication that let them diverge and caused the
bug this PR fixes. Extract a single _manifest_declared_template(pack_dir, name,
type) -> (entry, candidate) helper (candidate is the declared file only when it
is_file(); a declared-but-unusable file returns (entry, None) so callers skip the
convention fallback). resolve() and collect_all_layers() now both call it, so
their manifest-first resolution cannot silently diverge again.
Pure refactor, behavior-preserving: full test_presets.py (331) still passes,
including the directory-at-file:, missing-file, and manifest-file-wins cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(init): don't block on confirmation for 'init --here' without a TTY
When 'specify init --here' targets a non-empty directory without --force, it called typer.confirm() unconditionally. In a non-interactive session (no TTY -- CI, piped, agent) there is no input, so the prompt reads EOF and aborts unhelpfully (or blocks), with no actionable message. The named-project path already fails fast and points to --force; --here was the inconsistent outlier.
Guard the confirmation with the existing _stdin_is_interactive() helper: when non-interactive, print a clear 'directory not empty; re-run with --force' error and exit 1 instead of prompting.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(init): honor piped confirmation for 'init --here'; only fail-fast on empty stdin
The first version of this fix short-circuited on '_stdin_is_interactive()' (isatty) before typer.confirm, which broke 'init --here' when confirmation is piped (e.g. 'echo y | specify init --here' / CliRunner input='y\n') -- a non-TTY pipe with valid input was wrongly rejected, regressing test_init_here_without_force_preserves_shared_infra. Instead, call typer.confirm normally (piped 'y'/'n' is honored) and catch the Abort/EOFError it raises only when stdin is empty, converting that to the actionable '--force' guidance. This keeps the UX win for the no-input case without rejecting piped input.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(init): distinguish interactive cancel from no-input; defer merge warning
Address Copilot review on the --here non-empty path: (1) treat typer.Abort during an interactive confirm (e.g. Ctrl+C) as a normal cancellation (exit 0), and only emit the '--force' guidance + exit 1 when there is no TTY (empty stdin / EOF) -- no longer conflating the two; (2) move the 'will be merged / may overwrite' warning so it only shows when actually proceeding (force) or folded into the confirmation prompt, not on the fail-fast path where nothing is merged.
Piped confirmation (e.g. 'echo y | specify init --here') is still honored, which is why the prompt is attempted rather than refused outright when non-interactive -- the existing test_init_here_without_force_preserves_shared_infra pipes 'y' and must succeed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(init): fail fast on non-interactive --here instead of prompting
Per Copilot review: do not call typer.confirm when stdin is not a TTY -- an open-but-idle non-TTY stdin (CI/agent) could block on the prompt. When the directory is non-empty and --force is not given, fail fast with '--force' guidance unless an interactive terminal is present. Interactive confirm still offers the merge-but-preserve path (distinct from --force, which overwrites); a Ctrl+C there is treated as a normal cancellation (exit 0). The merge/overwrite warning is only printed when actually proceeding, not on the fail-fast path.
Updated the preserve-merge E2E test to simulate an interactive terminal so it exercises the confirm path (non-interactive sessions now require --force).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(init): honor piped y/n for 'init --here', error only on no-input
Per maintainer review: restore the second-revision shape. Calling
typer.confirm normally keeps 'echo y | specify init --here' reaching the
non-destructive preserve-merge path (and piped 'n' cancels with exit 0).
Only when no confirmation input is available at all (closed/empty stdin
-> typer.Abort/EOFError) is it converted into the actionable error that
points at --force. This drops the _stdin_is_interactive fail-fast that
broke the common piped-confirm idiom and made preserve-merge
interactive-only. The preserve test no longer needs to monkeypatch
_stdin_is_interactive - it passes on the real contract.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(init): preserve interactive-cancel semantics; fold merge risk into the prompt
Two review-driven refinements to the 'init --here' non-empty confirm, keeping
the maintainer-endorsed control flow (piped y/n honored; non-interactive EOF →
actionable --force error):
1. typer.confirm raises typer.Abort for BOTH an interactive Ctrl+C and an EOF on
closed/empty stdin. Catching it unconditionally reported 'no confirmation
input available, use --force' and exited 1 even when the user cancelled at a
real TTY. Branch on _stdin_is_interactive(): a TTY cancel is a normal exit 0
('Operation cancelled'); only non-interactive EOF becomes the --force error.
2. Fold the merge-risk warning into the confirmation question instead of printing
it unconditionally beforehand, so the EOF/no-input path (which exits without
changing anything) no longer prints a misleading 'will be merged' line first.
Adds test_init_here_interactive_cancel_exits_zero (fails before: exit 1 with
--force; passes after: exit 0, 'cancelled', pre-existing file untouched). The
non-interactive EOF and piped-y preserve-merge tests are unchanged and still pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(integrations): exit cleanly on unbalanced quote in --integration-options (#3457)
`_parse_integration_options` called `shlex.split(raw_options)` unguarded, so an
unbalanced quote in the flag value (e.g. `--integration-options='--commands-dir
"foo'`) made shlex raise `ValueError: No closing quotation` and a raw traceback
escaped — unlike every other bad-input path in this function (unknown option,
missing value, unexpected value), which print a message and exit 1.
Reachable from `specify init --integration-options=...` and every `specify
integration install/switch/upgrade/migrate --integration-options=...`.
Wrap the split in a try/except ValueError that prints a one-line error and
raises `typer.Exit(1)`, matching the existing loud-fail UX. Add a test asserting
the unbalanced-quote input raises `typer.Exit` with exit code 1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
kiro-cli confines all of its managed files to an isolated agent root
(`.kiro/`, with commands in `.kiro/prompts`) that no other integration
writes to, so it meets every documented criterion for multi-install
safety — but `KiroCliIntegration` never set `multi_install_safe = True`.
As a result, co-installing kiro-cli alongside any other integration left
`specify integration status` permanently in ERROR:
error unsafe-multi-install: Installed integrations are not all
declared multi-install safe: kiro-cli
`--force` bypasses the install-time gate but does not clear the status
error, and there is no flag or config to acknowledge it, so the error is
permanent while both integrations remain installed.
Set `multi_install_safe = True`. The registry's parametrized
multi-install-safe contract tests (static isolated root, distinct agent
roots / command dirs, disjoint manifests) now cover kiro-cli
automatically, and a focused regression test pins the declaration so a
future edit cannot silently drop it and reintroduce the error.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`FanInStep.validate()` and the engine's fan-in checks both reject a
non-list `wait_for`, but the engine's `execute()` path does not
auto-validate (see `WorkflowEngine.load_workflow`, whose docstring notes
the definition is "not yet validated"). On an unvalidated run, `execute`
iterated the raw value with `for step_id in wait_for`, with two bad
outcomes:
* a scalar (`wait_for: 5`, `wait_for: null`) raised `TypeError` and
took down the whole run — the engine invokes `step_impl.execute()`
with no surrounding try/except; and
* a string (`wait_for: stepA`) silently iterated its characters and
returned a join of empty results with a COMPLETED status — the exact
"silent empty result + COMPLETED" wiring bug the engine's own fan-in
validation comment warns against.
Guard `execute` to return a FAILED StepResult naming the type error
instead, mirroring the fan-out step's non-list `items` handling. A
missing `wait_for` key still defaults to an empty list (COMPLETED),
unchanged; the guard fires only on an explicit non-list value.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: bump version to 0.12.13
* chore: begin 0.12.14.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
`SwitchStep.validate()` already rejects a non-mapping `cases`, but the
engine's `execute()` path does not auto-validate (see
`WorkflowEngine.load_workflow`, whose docstring notes the definition is
"not yet validated"). On an unvalidated run, `execute` called
`cases.items()` on the raw value, so a list or scalar `cases` authoring
mistake raised `AttributeError` and took down the whole run — the engine
invokes `step_impl.execute()` with no surrounding try/except.
Guard `execute` to return a FAILED StepResult naming the type error
instead, mirroring the fan-out step's non-list `items` handling. The
expression is still evaluated first, so its value is surfaced in the
step output for downstream context.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: rewrite extension-relative subdir paths in generated command bodies
Extension command bodies reference bundled files relative to the
extension root (agents/, knowledge-base/, templates/, ...). Generated
SKILL.md and command files emitted those paths verbatim, so agents
resolved them against the workspace root where they do not exist.
Add CommandRegistrar.rewrite_extension_paths, which rewrites references
to subdirectories that actually exist in the installed extension to
.specify/extensions/<id>/..., and call it once in register_commands so
every output format and alias gets the fix. commands/, specs/ and
dot-directories are never rewritten.
Fixes#2101
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: only rewrite relative extension path references
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: use callable re.sub replacement for extension subdir rewrite
subdir and extension_id come from filesystem directory names and were
interpolated into a re.sub string replacement template. A directory name
containing a backslash (e.g. assets\q) would raise re.error: bad escape,
aborting command registration even when the body didn't reference it.
Use a callable replacement so these values are treated literally.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: make subdir rewrite regression test cross-platform
Renamed the test's subdir fixture from "assets\\q" to "assets[q]":
on Windows, backslash is a path separator, so mkdir would create
nested "assets/q" dirs instead of one literally-named directory,
and iterdir() would only discover "assets", never exercising the
rewrite. extension_id keeps a real backslash/"\\1" since it isn't
used to create a directory, still verifying the callable replacement
handles it literally. Added a sanity assertion for this assumption.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: apply extension subdir path rewrite in skills-mode renderer
register_commands() rewrote extension-relative subdir references
(agents/, knowledge-base/, etc.) via rewrite_extension_paths(), but
_register_extension_skills() - the separate renderer used for active
non-native skills agents (e.g. Claude with ai_skills: true) - never
called it. Generated SKILL.md files left agents/... and
knowledge-base/... unresolved, and mapped the extension's own
templates/ through the generic project-level rewrite instead of its
installed .specify/extensions/<id>/templates/ location.
Reuse the existing rewrite_extension_paths() helper in
_register_extension_skills() at the same point register_commands()
applies it (before resolve_skill_placeholders' generic rewrite), and
add a skills-mode regression test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: apply extension subdir path rewrite on preset restore/reconcile paths
_unregister_skills() restored extension-backed SKILL.md content via
resolve_skill_placeholders() without first calling
rewrite_extension_paths(), so removing a preset override that shadowed
an extension command restored the bare, unresolvable agents/... and
knowledge-base/... references. Carried extension_id/extension_dir
through _build_extension_skill_restore_index() and applied the same
rewrite used at initial registration before restoring.
Found the identical gap in _reconcile_composed_commands()'s non-skill
agent path: when a removed preset's command reverts to an extension
winner, register_commands_for_non_skill_agents() was called without
extension_id, so the rewrite never ran for plain command-file agents
either. Passed extension_id through there too.
Added regression tests for both restore paths (skills-mode and
non-skill-agent command files) in tests/test_presets.py.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: apply extension subdir path rewrite when composing over extension base
PresetResolver.resolve_content() read the effective base layer's raw
content directly via path.read_text() before composing append/prepend/
wrap overlays on top of it, and its outright-replace shortcut did the
same. When that base layer was extension-provided, neither read path
applied rewrite_extension_paths(), so composing a preset over an
extension command (or an extension winning outright through
resolve_content) left bare, unresolvable agents/... and
knowledge-base/... references in the composed output.
All three call sites (PresetManager._register_commands()'s composed
path, _reconcile_composed_commands()'s composed path, and skills-mode
reading the .composed file written by either) consume resolve_content's
return value, so fixing the read at its source covers command output,
skill output, and both initial-install and reconcile flows without
threading extension identity through each caller.
Tagged extension layers in collect_all_layers() with extension_id/
extension_dir, and added a _read_layer_content() helper in
resolve_content() that applies rewrite_extension_paths() whenever a
layer carries that extension identity — used at both raw-read sites
(outright-replace shortcut and composition base). Composing
(append/prepend/wrap) layers are never extension-provided (extensions
are always inserted with strategy "replace"), so no other read site
needs the rewrite.
Added regression tests: a parametrized resolve_content() test covering
append/prepend/wrap composing over an extension base, a skills-mode
test asserting the composed SKILL.md resolves the extension's subdir
references, and a non-skill-agent (Gemini) install-time test matching
the reported live repro.
Assisted-by: GitHub Copilot (model: Claude Sonnet 5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(templates): point constitution sync checklist at installed command files
The consistency-propagation checklist told the agent to read
.specify/templates/commands/*.md, but specify init never creates that
directory — command templates are rendered straight into the
agent-specific directory (.github/prompts/, .claude/commands/, ...).
The checklist step could therefore never run against real files.
Point it at the installed speckit.* command files for the active agent
instead.
Fixes#660
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(templates): cover hyphenated and skills-mode command filenames
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(templates): use actual integration output directories in examples
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(templates): cover skills-based command layouts in sync checklist
Copilot skills mode installs speckit-<name>/SKILL.md under .github/skills/,
not .github/agents/. Mention both directories and the SKILL.md layout.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(templates): restore hyphenated speckit-* naming in sync checklist
The previous commit dropped the speckit-* flat-file variant used by
Cline and others while adding the SKILL.md layout. Name all three:
speckit.*, speckit-*, and speckit-<name>/SKILL.md.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: clarify agent-specific reference phrasing in constitution template
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(workflows): make shell step timeout configurable (#3327)
The `shell` step hardcoded a 300s subprocess timeout, so any command
that legitimately runs longer than five minutes (a full build, a linter
aggregator, an integration-test target) was killed with TimeoutExpired
and failed the whole run, with no YAML knob to raise the limit.
Add an optional `timeout` field (seconds) that defaults to 300 for
backward compatibility and is threaded through to `subprocess.run`. The
timeout failure message now reports the configured value instead of a
hardcoded 300. `validate` rejects a `timeout` that is not a positive
number (bool is rejected explicitly, since it is an int subclass but a
config error rather than a duration).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* test(workflows): cover non-finite timeout rejection in shell step
The isfinite guard added in 955d46a rejects YAML .inf/.nan timeouts, but no test asserted it. inf and nan are floats that pass a plain > 0 check (nan <= 0 is False), so without an explicit case a regression could silently reaccept them and crash subprocess.run(timeout=...) at runtime. Addresses the remaining Copilot review comment on PR #3328.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(workflows): document configurable shell step timeout
Address Copilot review feedback on #3328: the per-step `timeout`
option was not reflected in the public workflow docs. The Shell Steps
section only showed `run:`, so readers couldn't discover `timeout:`,
its unit (seconds), or its default (300).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* refactor(workflows): consolidate shell-step timeout validation into one path
Address Copilot review feedback on #3328:
- Remove the dead "fall back to default" timeout block in execute(): it
re-read `timeout` from config immediately after, so the fallback was
discarded and its comment contradicted the new fail-on-invalid behavior.
- Extract a single `_timeout_error()` helper shared by execute() and
validate() so both reject the same values with the same message, instead
of two drifting copies of the check.
- Hoist the duplicated inline `import math` to module scope.
- Add test_execute_fails_cleanly_on_invalid_timeout: asserts execute()
fails the step (rather than raising) on an unvalidated string/bool/inf/0
timeout, covering the engine-skips-validate path Copilot flagged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Readers were replacing vX.Y.Z with bare versions like 0.12.11,
which fails because git tags are named v0.12.11.
Assisted-by: Cursor Grok 4.5 (supervised)
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(workflows): don't crash on membership test against a non-iterable
the `in` / `not in` operators in _evaluate_simple_expression only guarded
`right is not None`, so `left in right` still raised a raw TypeError when the
right operand was any other non-iterable (int, bool, float). a condition like
`{{ inputs.tag in inputs.count }}` where count is a number crashed the whole
workflow run instead of evaluating.
nothing is contained in a non-iterable, so treat membership as False (`not in`
as True) via a new _safe_membership helper that swallows TypeError. this
generalizes the old None guard and mirrors _safe_compare, which already
catches TypeError for the ordering operators.
added a regression test; confirmed it fails on the pre-fix code (raw
TypeError) and that genuine list/substring membership still works.
* address review: float membership case + broaden _safe_membership docstring
- add a float right-operand assertion so the test matches its comment (was
claiming float coverage while only exercising int/bool/None).
- reword the _safe_membership docstring to describe TypeError generally
(non-iterable right is the common case, but also e.g. an unhashable left
against a set) rather than implying only the right operand matters.
* fix(workflows): if-step validate accepts falsy non-list else
IfThenStep.validate() guarded the 'else' branch with
'if else_branch and not isinstance(else_branch, list)'. The leading
truthiness check short-circuits for falsy non-list values (False, 0,
'', {}), so a malformed else-branch passes validation and is then
silently skipped at runtime. The sibling 'then' branch is validated
strictly; 'else' now matches by switching to an 'is not None' guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(workflows): cover explicit else:None and missing-else separately
Per Copilot feedback: the parametrized valid-else test omitted the
'else' key when the value was None, so it covered only the missing-else
case, not an explicit 'else: None'. Set 'else' explicitly (including
None) in the parametrized test and add a dedicated missing-else test, so
both accepted shapes are pinned.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore: bump version to 0.12.12
* chore: begin 0.12.13.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
The set-priority skip guard 'isinstance(raw_priority, int) and
raw_priority == priority' treats a stored boolean as a match because
isinstance(True, int) is True and True == 1 (False == 0). So a corrupted
boolean priority short-circuits to 'already has priority N' and is never
rewritten to a real int — contradicting the adjacent comment that
promises corrupted values get repaired. Exclude bools explicitly,
mirroring normalize_priority's own bool guard.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Same bool-is-int trap as the extension set-priority command: the skip
guard 'isinstance(raw_priority, int) and raw_priority == priority' treats
a stored boolean as a match (isinstance(True, int) is True, True == 1),
so a corrupted boolean priority reports 'already has priority N' and is
never rewritten to a real int — contradicting the adjacent comment.
Exclude bools explicitly, mirroring normalize_priority's bool guard.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The while/do-while loop cap guard 'not isinstance(max_iters, int) or
max_iters < 1' does not fall back to the default for a boolean
max_iterations: isinstance(True, int) is True and True < 1 is False. The
loop then runs range(max_iters - 1) == range(True - 1) == range(0),
capping at a single iteration instead of the default 10. Exclude bools,
mirroring the merged while/do-while validators (#3237) and this
function's own continue_on_error bool handling. execute() does not
auto-validate, so this engine guard is the only defence.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The 'bundle update' command accepts --integration (verified via
'specify bundle update --help' and the command signature), used as the
integration override when the project's active integration can't be
detected. The Update Bundles options table in reference/bundles.md
omitted it, listing only --all and --offline — unlike the install/init
tables which already document --integration. Add the missing row.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(workflows): harden catalog.py against mis-shaped registry & non-string fields
Two robustness gaps where WorkflowRegistry/WorkflowCatalog diverged from
their StepRegistry/StepCatalog siblings, which already guard both:
- WorkflowRegistry._load returned json.load() verbatim, so a JSON-valid
but mis-shaped registry (a list root, or a dict lacking a 'workflows'
mapping) made is_installed/get/list/remove/add crash with
TypeError/KeyError. Mirror StepRegistry._load: validate the shape and
reset to default, and widen the except tuple to OSError/UnicodeError.
- WorkflowCatalog.search joined name/description/id without coercion, so a
null or non-string field raised TypeError. Coerce with str(... or '')
exactly as StepCatalog.search does.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(workflows): tighten mis-shaped-registry assertions
Per review: WorkflowRegistry.list() always returns a dict, so assert
'== {}' directly (the previous '== {} or == []' called list() twice and
admitted a shape it never returns), and reference
WorkflowRegistry.SCHEMA_VERSION instead of hard-coding '1.0'.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(bundle): resolve file:// download_url via the file-URL helper
_download_manifest built the local path from raw parsed.path, which
keeps the leading slash of file:///C:/x (yielding a \C:\x path that
never exists on Windows) and skips percent-decoding (my%20bundles stays
encoded on every OS) — so a catalog entry whose download_url is the
canonical URI Python itself produces via Path.as_uri() always fails
with 'Bundle manifest not found'. Route the file scheme through the
existing bundler.services.adapters._file_url_to_path helper, which
already handles drive letters, UNC hosts, and percent-decoding for
catalog file:// URLs (make_catalog_fetcher). The bare-path branch is
unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(bundle): reject file:// / local download_url; catalog URLs are HTTPS-only
Per maintainer review (route B): file:// in a catalog download_url was
never intended — catalog URLs are HTTPS-only (http for localhost) across
the extensions/presets/workflows catalog systems, and disk installs go
through the positional path (specify bundle install <path>), handled by
_local_manifest_source before catalog resolution. Remove the
file:///bare-path branch from _download_manifest and route everything
through _download_remote_manifest (HTTPS-only via _require_https), with an
actionable error pointing at the positional install. Invert the file://
tests to assert rejection (+ a positional-path resolution test), and
migrate the three bundle-info contract tests off local download_urls onto
an HTTPS-only entry with a mocked manifest fetch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(bundle): validate HTTPS before the offline gate in _download_manifest
Per review: for a non-local download_url the offline check ran before any
URL validation, so an invalid/non-HTTPS scheme surfaced a misleading
'Network access disabled' error under --offline when the real problem is
the URL would be rejected even online. Call _require_https before the
offline gate so the correct error is reported in every mode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(bundle): reword non-HTTPS download_url error to not mislabel scheme-less URLs
A scheme-less download_url (urlparse scheme == "") can be a bare
filesystem path OR a missing-scheme value like 'example.com/foo.zip',
not necessarily file://. Reword the reject error to state the real
HTTPS-only constraint and enumerate what is rejected (file://, local
path, scheme-less), instead of labeling every case 'local/file://'.
Behavior unchanged; the 'bundle install' actionable hint is preserved,
so the existing reject-path tests still pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(extensions): handle prefix-colliding env vars in _get_env_config
_get_env_config built the nested dict with 'if part not in current:
current[part] = {}' and an unconditional leaf assignment. Two env vars
that collide on a prefix — e.g. SPECKIT_X_CONNECTION and
SPECKIT_X_CONNECTION_URL — then either crash (scalar processed first:
the walk indexes into a str -> TypeError 'str object does not support
item assignment') or silently clobber the nested dict (scalar processed
last). Via should_execute_hook's blanket except, the crash silently
disables every config-based hook for the extension. Guard the walk and
the leaf assignment with isinstance checks so a colliding scalar yields
to the nested dict; result is order-independent
({'connection': {'url': ...}} either way), matching _merge_configs'
dict-preserving semantics.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(extensions): assert via public should_execute_hook, not private helper
Per review: the colliding-env hook test described should_execute_hook
swallowing the TypeError, but asserted on the private _evaluate_condition.
Assert on the public should_execute_hook instead — it returns False
(silently disabled) before the fix and True after, matching the
real-world failure mode and not coupling to a private helper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(extensions): ignore malformed env var names in _get_env_config
Per review: a name like SPECKIT_<EXT>_ (no key) or with consecutive
underscores produced empty path components, creating surprising entries
under an empty key (env_config[''] = ...). Filter out empty parts and
skip the variable entirely when nothing remains.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* docs: document copilot skills mode (--skills) and markdown deprecation
as of #3256 (v0.12.3) the copilot integration supports a skills mode via
`--integration-options "--skills"`, and installing without it warns that the
legacy markdown default is being phased out. this was undocumented:
- the copilot row in the supported-agents table had an empty notes cell while
other skills-capable agents describe their behavior there.
- `--skills` was missing from the integration-specific options table (only
generic and kimi were listed).
fill both. wording matches the code: skills scaffold as speckit-<name>/SKILL.md
under .github/skills/ and are invoked as /speckit-<name>; without the flag the
install emits the deprecation warning from _warn_legacy_markdown_default().
fixes#3300
* docs: describe copilot default as legacy markdown mode (.agent.md + .prompt.md)
the copilot rows said the default installs .agent.md files, but the default
scaffold also writes companion .prompt.md files under .github/prompts/. also
reworded to 'legacy markdown mode' to match the deprecation warning users
actually see and to avoid ambiguity, since skills are markdown too.
* docs: spell out copilot legacy markdown paths and use <command> in copilot rows
address the follow-up copilot review: name where the default scaffold writes
files (.github/agents/*.agent.md plus .github/prompts/*.prompt.md and a
.vscode/settings.json merge), and switch speckit-<name> to speckit-<command>
to match the rest of the table. verified all three paths against the copilot
integration source.
* docs: use --integration-options="..." form in copilot notes cell
match the equals form the rest of the doc uses (generic row, the options
table, and the install example) so readers don't mistake the quotes for part
of the value. addresses copilot review feedback.
* chore: bump version to 0.12.11
* chore: begin 0.12.12.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(agent-context): discover nested plan.md in scoped layouts (#3024)
The agent-context updater only looked for plan.md one level deep
(specs/*/plan.md), so scoped layouts created via SPECIFY_FEATURE_DIRECTORY
(specs/<scope>/<feature>/plan.md) were never picked up and no plan
reference was written into the context file.
Recurse into specs/ in both the bash (rglob) and PowerShell (-Recurse)
scripts. In the PowerShell script, also replace
[System.IO.Path]::GetRelativePath, which is .NET Core 2.1+ only and throws
under Windows PowerShell 5.1 (.NET Framework); the exception was swallowed
by the surrounding try/catch, leaving the plan path empty on 5.1 even when
a plan was found. Compute the project-relative path by stripping the root
prefix instead.
Add regression tests for both scripts covering nested discovery.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(agent-context): guard mtime plan discovery against symlink escape
Address Copilot review feedback on #3301:
- bash updater: the mtime fallback filtered candidates lexically via
relative_to() on the *unresolved* path, so a plan reached through a
specs/ symlink pointing outside the project could be selected and emit
an in-project-looking path. Resolve each candidate and keep only those
whose resolved path stays under root before picking the newest.
- test: the nested-plan PowerShell regression targets a Windows
PowerShell 5.1 (.NET Framework) failure mode, but ran whatever
POWERSHELL resolved to (prefers pwsh). Prefer powershell.exe on Windows
so the 5.1-only compat fix is actually exercised.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(agent-context): note recursive plan.md discovery in update command
Auto-detection now recurses (`specs/**/plan.md`) to support nested scoped layouts created via SPECIFY_FEATURE_DIRECTORY (#3024). The update command doc still described the old one-level `specs/*/plan.md` glob, which could mislead users troubleshooting plan detection. Addresses Copilot review feedback on PR #3301.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
find_entries_for_url did (urlparse(url).hostname or "").lower() unguarded. a
malformed authority (e.g. an unterminated ipv6 bracket "https://[::1") makes
urlparse/hostname raise ValueError, so instead of the empty list the function
already returns for a host-less url, a raw ValueError leaked out of the shared
http client (build_request / open_url call this before any url validation).
no auth entry can match such a url, so treat it like the host-less case and
return no matches. added a regression test over an unterminated bracket and a
bracketed non-ip host; confirmed it fails on the pre-fix code.
CatalogStackBase._validate_catalog_url read parsed.hostname, which raises
ValueError on a malformed authority (e.g. an unclosed ipv6 bracket
"https://[::1"). every other reject path raises the class catalog error, so
the raw ValueError leaked to the caller. wrap the parse + hostname access and
convert ValueError to the normal error via cls._error.
twin of the bundler fix in adapters._validate_remote_url.
adapters._validate_remote_url reads parsed.hostname, which raises ValueError
on a malformed authority (e.g. an unclosed ipv6 bracket https://[::1). the
function's contract is to raise BundlerError for any bad url - every other
reject path does - so the raw ValueError leaked to the caller and crashed the
fetch instead of failing cleanly. wrap the parse and convert to BundlerError.
bundler sibling of #3369, which fixed the cli extension/preset/workflow add
paths but not this validator. added a regression test that fails pre-fix.
* fix(workflows): validate scalar types before string operations in workflow validation
YAML parses unquoted scalars like version: 1.0 and id: 123 as
float/int, which crashed validate_workflow and workflow add with raw
tracebacks. Type-check id, name, version and step ids before regex
and string operations so these surface as validation errors. Accept
an unquoted schema_version: 1.0 instead of printing a self-identical
rejection message.
Fixes#3420
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): treat falsey non-strings as type errors, not missing fields
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): only accept schema_version 1.0 so the error message is accurate
The check also accepted "1" while the error said Expected '1.0'.
Unquoted YAML 1.0 still works via str(); plain 1 is now rejected with
the message that matches.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The note told readers to "See .specify/templates/plan-template.md for
the execution workflow" — that path is the file itself. The execution
workflow lives in the plan command's definition, which the note already
names via the __SPECKIT_COMMAND_PLAN__ placeholder. Point there instead
of at a self-reference.
Fixes#1148
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: bump version to 0.12.10
* chore: begin 0.12.11.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
The plan command's outline listed two bullets labeled Phase 1 and the
completion report said the command ends after "Phase 2 planning",
but the Phases section only defines Phase 0 and Phase 1. Phase 2
(tasks.md) belongs to the tasks command, as plan-template.md states.
The duplicated "Phase 1: Update agent context" bullet is a leftover
from before the agent-context extension: core no longer ships an agent
script, and the update runs via the extension's after_plan hook.
Fixes#1036
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`-Number` defaults to 0, so the previous `-eq 0` / `-ne 0` checks could
not distinguish an unset flag from an explicit `-Number 0`: a user
requesting branch `000-...` was silently routed into auto-detection.
Switch both checks to `$PSBoundParameters.ContainsKey('Number')`, which
tests whether the flag was actually supplied — mirroring the bash twin's
empty-string sentinel (`[ -z "$BRANCH_NUMBER" ]` / `[ -n ... ]`).
Add a parity regression test to both TestCreateFeatureBash and
TestCreateFeaturePowerShell asserting `--number 0` / `-Number 0` yields
`000-zero`.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test_template_renders_python_invocation monkeypatches shutil.which to
return /usr/bin/python3, but on Windows resolve_python_interpreter guards
the which() result with a real _interpreter_runs subprocess probe (#3304).
The mocked /usr/bin/python3 path does not exist on a Windows runner, so the
probe fails, the resolver falls back to sys.executable (a ...python.exe
path), and the python3-anchored regex assertion fails.
Patch _interpreter_runs to return True in the _pin_interpreter fixture so
the resolved interpreter token stays python3 across all platforms, keeping
the #3304 production guard intact while making the assertion deterministic.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* feat(workflows): make shell step timeout configurable
The shell step hardcoded a 300s subprocess timeout, killing any
legitimate long-running QA command. Read an optional timeout field
(seconds, positive integer, default 300) and validate it.
Fixes#3327
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* style: multi-line timeout validation, assert status in default test
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Guard against unvalidated timeout in ShellStep.execute()
The engine does not auto-validate step config, so a string or null
timeout would reach subprocess.run() and crash the run with a
TypeError. Fall back to the 300s default for malformed values,
mirroring how the engine treats unvalidated continue_on_error.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: find plans in nested spec directories
The agent-context mtime fallback used a one-level specs/*/plan.md
glob, so scoped layouts (specs/<scope>/<feature>/plan.md via
SPECIFY_FEATURE_DIRECTORY) never matched and the SPECKIT block was
written without a plan path. Recurse in both script variants and
update the command doc wording.
Fixes#3024
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor: pick newest plan with max(), align doc wording
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(templates): add py: lines to command templates' scripts frontmatter
Every templates/commands/*.md with a scripts: block now declares a py:
variant so --script py renders a Python invocation via the existing
interpreter-prefixing in process_template.
Fixes#3283
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: install scripts/python for the py script type
install_shared_infra mapped every non-sh script type to powershell, so
--script py rendered invocations pointing at files that were never
installed. Map py to the python variant dir and skip __pycache__
artifacts during the copy.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: read templates with explicit utf-8 encoding
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: keep py: lines standalone, enforce script existence in tests
Drop the plan/tasks py: lines that referenced scripts shipping in the
core port (#3280); they move to that PR. Tests now assert every py:
line points at a script the repo ships, so a dangling reference can
never merge green.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: bump version to 0.12.9
* chore: begin 0.12.10.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(integrations): skip Windows Store python3 alias stub in resolve_python_interpreter
On stock Windows, python3 on PATH is the Microsoft Store App Execution
Alias stub: it exists but only prints an installer hint and exits
non-zero, so generated {SCRIPT} invocations for the py script type were
broken. Verify the found interpreter actually runs before accepting it,
on Windows only, mirroring the parse-success-not-availability approach
of #3312/#3320 for the sh scripts. POSIX keeps the plain existence
check. sys.executable remains the fallback and is always live.
Fixes#3383
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(integrations): probe interpreter isolated and without site, discard I/O
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: pin POSIX platform in PATH-resolution tests
The tests fake shutil.which with POSIX paths; on Windows CI the real
sys.platform made the stub probe run against those fake paths and
fall through to sys.executable.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
yaml.safe_dump with default_style='"' replaces the hand-rolled quote
helpers in base.py and hermes, so newlines and control characters in
template descriptions round-trip instead of producing unparseable YAML.
Fixes#3391
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): apply chained expression filters left-to-right
The pipe-filter parser in `_evaluate_simple_expression` split the
expression only at the *first* top-level `|` and treated the whole
remainder as a single filter. So a filter chain like
`{{ inputs.rows | map('name') | join(', ') }}` handed
`map('name') | join(', ')` to one filter, where the `(\w+)\((.+)\)`
regex mangled it and raised `ValueError`.
This broke the canonical use of `map`: it returns a list, and `join`
is the only filter that renders a list to a string, so the two are
meant to be chained. Chaining was impossible for every registered
filter.
Split the pipe segments at the top level (quote/bracket aware, so a
literal `|` inside a quoted argument like `join(' | ')` is preserved)
and fold each filter over the running value. The single-filter logic
is extracted verbatim into `_apply_filter`, so all existing strict
handling (`from_json` arity, unsupported-form vs unknown-filter
messages) is unchanged and now applies to every link in the chain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(scripts): resolve invoke_separator by parse success, add awk fallback (#3304)
`get_invoke_separator` in common.sh selected its JSON parser by tool
*availability* (`command -v python3`) rather than parse *success*, and had
no text fallback after the python3 branch. On stock Windows + Git Bash —
no jq, and `python3` resolving to the Microsoft Store App Execution Alias
stub that passes `command -v` but exits 49 at runtime — it silently fell
back to "." even for `-`-separator integrations (e.g. forge, cline). The
observable result was wrong command hints in error messages, such as
`/speckit.plan` instead of `/speckit-plan`, in check-prerequisites.sh and
setup-tasks.sh.
This is the same existence-vs-runtime pattern fixed for the feature.json
parser in #3304's primary report; the reporter explicitly asked that other
python3 call sites be checked. The two remaining sites (resolve_template,
resolve_template_content) already fall through on failure and are unaffected.
- Restructure the jq -> python3 chain to fall through on parse failure,
gated on a `parsed` success flag rather than exclusive elif branches.
- Make the python3 branch signal failure (sys.exit(1)) instead of printing
"." so a stub failure falls through instead of being accepted.
- Add an awk text fallback (portable, no gawk-only whole-file slurp) that
reads the active integration key and its invoke_separator, handling both
pretty-printed (the written form) and compact JSON. Malformed input
safely defaults to ".".
Regression test simulates the broken-stub environment (jq + python3 stubs
that exit 49 on PATH) and asserts the `-` separator is still recovered.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(scripts): close awk END block so invoke_separator fallback works
The awk text fallback in get_invoke_separator was missing the closing brace for its END block, so awk aborted with a syntax error on every invocation and the separator silently stayed at the default '.'. On environments with neither jq nor a working python3 (stock Windows + Git Bash, the exact case this fallback exists for) a '-'-separator integration like forge produced a wrong command hint. The bug escaped CI because the test that exercises it is gated on working bash and skips on the Windows dev shell.
Also harden the fallback per review: use the portable '[-.]' character class (a leading '-' inside '[]' can be read as an ill-defined range on some awk builds), and correct the test docstring, which said 'with no jq' though the helper installs a present-but-failing jq stub. Addresses Copilot review feedback on PR #3320.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(shared-infra): refresh_shared_templates preserves recovered user files
refresh_shared_templates skipped a shared template only when it was
untracked or modified, ignoring the manifest's is_recovered marker that
install_shared_infra already honors. So a pre-existing user template
(adopted via record_existing(recovered=True), hence tracked and
hash-unmodified) was silently overwritten with bundled content on
refresh — the exact data-loss class that #2918 fixed for
install_shared_infra. Add the is_recovered check to the skip predicate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(shared-infra): include recovered files in refresh skip warning
The skip predicate now also skips recovered (pre-existing user) files,
so the warning saying only 'modified or untracked' could mislead a user
into thinking they edited a file that was simply recorded as recovered.
Reword to 'modified, untracked, or preserved (recovered)'.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(agents): resolve skill placeholders in Goose (yaml) command output
CommandRegistrar.register_commands resolves {SCRIPT}/__AGENT__ and the
$ARGUMENTS placeholder in the markdown and toml branches, but the yaml
branch (Goose recipes) called render_yaml_command directly, skipping
both. So extension/preset command bodies installed for Goose kept literal
{SCRIPT}, __AGENT__, and repo-relative script paths in the generated
.goose/recipes/*.yaml prompt. Mirror the markdown/toml branches: run
resolve_skill_placeholders + _convert_argument_placeholder on the body
before render_yaml_command.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(goose): assert positive placeholder replacements in recipe prompt
Per review: parse the generated recipe with yaml.safe_load and assert the
prompt contains the resolved values (.specify/scripts/, 'agent goose',
{{args}}), not merely that the literal tokens are absent — a wrong output
that happens to omit the exact strings would otherwise pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(bundler): enforce version pin on bundled preset/extension installs
The bundled install branch of _PresetKindManager/_ExtensionKindManager
called install_from_directory and returned before _assert_pinned_version,
so a bundle manifest pinning e.g. 2.0.0 would silently install the
bundled asset's own version (1.0.0) — the pin was only enforced on the
catalog path. _WorkflowKindManager already enforces it unconditionally.
Read the bundled asset's declared version from its manifest (best-effort;
None => cannot enforce, matching the catalog 'advertises no version'
escape hatch) and call _assert_pinned_version before install, in both
bundled branches.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(bundler): address review on bundled version-pin check
- Make _assert_pinned_version's error source-agnostic ('resolved version'
/ 'the source') so bundled preset.yml/extension.yml mismatches read
correctly, not just catalog ones.
- Type-guard _bundled_manifest_version: only a non-empty string version is
usable; missing/non-string/whitespace -> None ('cannot enforce').
- Add bundled-preset success-path test (matching pin + version=None both
proceed to install_from_directory), mirroring the extension test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* test: isolate integration test home
Assisted-by: Codex (model: GPT-5, autonomous)
* test: assert integration home isolation
Assisted-by: Codex (model: GPT-5, autonomous)
* test: extend integration home isolation to module-scoped setup
Address Copilot review on #3144.
Add a session-scoped autouse fixture so HOME/USERPROFILE/XDG are redirected
for setup that runs outside a test function (e.g. the module-scoped status_*
fixtures in test_integration_subcommand.py that run `specify init` before any
per-test isolation applies). The function-scoped fixture still overrides HOME
per test.
Also assert Path.home() resolves to the isolated home, since most integrations
(Hermes, catalog) read the home via that API rather than the env vars directly.
* chore: bump version to 0.12.8
* chore: begin 0.12.9.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Add LLM Wiki extension to community catalog
Add wiki extension submitted by @formin to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table
Closes#3319
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: limit catalog.community.json changes to wiki entry + timestamps only
Reverts the unintended reordering and reformatting of existing extensions
(aide, checkpoint, critique, threatmodel, etc.) and companion's tools array.
Only the new wiki entry and updated_at timestamps are now changed.
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* docs: document missing flags and integrations
* docs: remove invalid --refresh-shared-infra from upgrade command
* docs: address PR feedback for extension and integration flags
* docs: reorder extension add options to match CLI help
* feat(extensions): port update-agent-context to Python
Ports the agent-context extension updater to a single Python script,
per #3281 and the check-prerequisites PoC pattern from #3302. The bash
version already ran its core logic through embedded Python heredocs, so
the port lifts that logic into a standalone script. Parity tests run
bash and Python side by side and compare output and resulting
context-file bytes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(extensions): match bash case-insensitivity on MSYS, test unparseable config gate
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): fall through to grep/sed when python3 is a broken stub in feature.json parser
read_feature_json_feature_directory picked its json parser by availability
(if jq / elif python3 / else grep-sed). on windows `python3` usually resolves
to the microsoft store app execution alias stub: it satisfies `command -v` but
fails at runtime (exit 49). the elif selected it, the runtime failure was
swallowed to _fd='', and the grep/sed last resort was never reached - so a
valid .specify/feature.json read as empty and every setup-plan / setup-tasks /
check-prerequisites call errored with "Feature directory not found" right after
a successful `specify init --script sh`.
change selection from availability to parse success: try jq, then python3 only
if still empty, then grep/sed only if still empty. a parser that exists but
produces nothing now falls through instead of terminating the chain.
the write path (_persist_feature_json) already uses jq-or-printf with no
python3, so it was unaffected; only the read path needed this.
add a regression test that puts a python3 stub (exit 49, like the store alias)
first on PATH and asserts setup-plan.sh still resolves the feature via the
grep/sed fallback.
fixes#3304
* test: shadow jq so the broken-python3 fallback test actually exercises it
the test claimed it dropped jq so the parser chain would reach python3 and
then grep/sed, but it only prepended the python3 stub dir to PATH. on a
runner with jq installed, read_feature_json_feature_directory parses via jq
and never reaches the fallback the test is meant to cover.
add a failing jq stub alongside the python3 stub so the chain is forced
through jq -> python3 -> grep/sed regardless of what the runner has installed.
* fix(toml): escape control characters so generated command files parse
both toml renderers (TomlIntegration._render_toml_string for gemini/tabnine
and CommandRegistrar.render_toml_command for extension/preset commands) wrote
control characters raw into a multiline or basic string. toml forbids literal
control chars (U+0000-U+001F except tab/newline, and U+007F) in every string
form, and a bare CR that is not part of a CRLF pair, so a description or body
containing one produced a .toml file that fails to parse.
route any value with such a character to a fully-escaped basic string that
emits the leftover control chars as \uXXXX. added regression tests that
round-trip through tomllib.
* refactor(toml): centralize control-char escaping in one shared helper
the control-char detection and basic-string escaping added for both toml
renderers were copy-pasted into agents.py and integrations/base.py. move the
two functions into specify_cli/_toml_string.py and have both renderers
delegate to it, so the escaping rules can't drift apart later.
no behavior change; both renderers now reference the same implementation.
* fix(cli): exit cleanly on malformed IPv6 URLs in extension/preset/workflow add
extension add --from, preset add --from, and workflow add <url> parsed
the user-supplied URL with a bare urlparse before their HTTPS/host
validation, so an unclosed IPv6 bracket escaped as a raw ValueError
traceback. Wrap each parse and emit the surrounding validation's clean
error style + typer.Exit(1) instead.
Fixes#3368
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(cli): convert malformed redirect URLs to URLError in shared redirect handler
Parse the redirect target once in _StripAuthOnRedirect.redirect_request
before the validator and stdlib handler run, converting ValueError into
URLError which every download path already catches. Also escape from_url
in the preset install message so IPv6 brackets don't break Rich markup.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
resolve_github_release_asset_api_url's is_ghes branch built the authority
with 'parsed.port', which raises ValueError on a malformed port (e.g.
host:notaport). The function's contract is to resolve or return None,
never raise — every other unresolvable case returns None. An allowlisted
GHES host with a bad port therefore crashed the caller. Read parsed.port
defensively and return None on ValueError.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
manifest.py::_sha256 does an unguarded open(). check_modified() and
uninstall() both call it on a readable-but-unopenable regular file
(e.g. permission denied) without catching OSError, so
'specify integration upgrade/uninstall/switch' surface a raw
PermissionError traceback. Guard both call sites: in check_modified()
treat an unreadable file as modified (consistent with the adjacent
symlink / non-regular-file handling); in uninstall() treat it as skipped
and preserve it (mirroring the existing path.unlink() OSError guard just
below). The force short-circuit is unchanged.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* chore: bump version to 0.12.7
* chore: begin 0.12.8.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
On refresh (bundle update), install_bundle iterated only the new plan's
components, so a component the previous version owned but the new one no
longer ships was left installed on disk while being dropped from the
rewritten record (contributed only holds plan.components). With no
record referencing it, remove_bundle could never clean it up —
permanently orphaned, violating the provenance invariant (FR-022). After
the component loop, when refresh and a prior record exists, uninstall
each previously-owned component absent from the new plan — unless another
bundle still needs it (components_still_needed refcount, mirroring
remove_bundle), in which case it stays installed and is simply
de-attributed. Runs inside the existing try so a failed removal takes the
same no-record-written rollback path.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
workflow run/resume --json is contracted to emit a single JSON object on
stdout, but every error path (workflow-not-found, invalid workflow,
validation failure, execute/resume failure, and the shared
_parse_input_values invalid-input error) used console.print, landing the
human error text on stdout and corrupting the machine-readable stream.
Route those messages through err_console when --json is set (a no-op for
normal text mode), mirroring the stderr-only error routing already used
by 'specify bundle' (_fail) and err_console elsewhere in this module.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
FanInStep.validate() only checked wait_for, so a non-mapping 'output'
(a list or scalar) validated clean; execute() then silently coerces it
to {}, so the author's declared aggregation keys vanish with COMPLETED
status and no diagnostic. Reject a non-mapping output at validation,
mirroring the command-step (#3262) non-mapping fix. execute()'s
defensive coercion is left in place for unvalidated callers.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ShellStep.validate() only checked that 'run' was present, so run: (null)
or a GitHub-Actions-style list validated clean; execute() then
str()-coerces the value and invokes it under shell=True, literally
running 'None' or "['echo', 'hi']" as a command. Add a type check after
the presence check, mirroring the command-step (#3262) and gate options
validation. Expression strings ('{{ ... }}') are strings, so they stay
valid.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
AgyIntegration.build_exec_args returned [exe, '--print', prompt] without
calling _apply_extra_args_env_var(), so the documented per-integration
extra-args env hook was silently dropped for agy — same class as the
cursor-agent fix#3265. Append the hook after the positional prompt,
matching the devin integration's shape. agy still ignores model/output
as before.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(integrations): add post_process_command_content() hook for all format types
Add post_process_command_content(self, content: str) -> str to IntegrationBase
with a no-op default. Wire it into register_commands() for non-skills format
types (Markdown, TOML, YAML) after format rendering, before writing to disk.
Also applies to aliases rendered via the inject_name path (cline, forge).
Skills-format agents are excluded to preserve the existing
post_process_skill_content() path and avoid double-processing.
This gives extension authors a clean per-agent content transformation seam
for all 21 non-skills integrations that previously had no post-processing hook.
Ref: #3303
Assisted-By: 🤖 Claude Code
* fix: initialize _integration before conditional branch
Prevents potential UnboundLocalError if the non-skills guard is
refactored without updating the alias path reference.
Assisted-By: 🤖 Claude Code
* chore: bump version to 0.12.6
* chore: begin 0.12.7.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(bundler): validate catalog URLs in `catalog add` (HTTPS-only, require host)
add_source persisted remote catalog URLs without the HTTPS/host checks
that specify_cli.catalogs (#3210) and the bundler adapters (#3333)
enforce, and an unclosed IPv6 bracket escaped as a raw ValueError.
Mirror the catalogs.py validation for http(s) schemes and wrap urlparse
so malformed input raises BundlerError.
Fixes#3366
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: correct config filename and validation reference in comment
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
HermesIntegration.build_exec_args routed argv[0] through
_resolve_executable() but never called _apply_extra_args_env_var(), so
the documented per-integration extra-args env hook was silently dropped
for hermes — the same class of bug fixed for cursor-agent in #3265.
Insert the hook after the base 'chat -Q' command and before Spec Kit's
canonical -m/--json/-s/-q flags (mirrors opencode), so operator args
can't displace or clobber the canonical flags.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ConfigManager._load_yaml_config returned yaml.safe_load(...) or {}, which
only guards falsy roots — a truthy non-mapping root (a YAML list or
scalar) flows straight into _merge_configs, whose .items() raises
AttributeError. get_config()/has_value()/get_value() then crash, and via
should_execute_hook's blanket 'except Exception: return False' every
config-based hook condition for that extension is silently disabled.
Coerce a non-dict root to {}, mirroring the existing non-dict-root guard
in get_project_config(). Hardens all three call sites in one place.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
the goose recipe renderer emitted the prompt body under a bare '|' block
scalar. yaml infers a plain block scalar's indentation from its first
non-empty line, so a command body whose first line is itself indented (a
markdown code block, a nested list item) made the parser expect that deeper
indent for the whole block and reject the later, shallower lines - the
generated .goose recipe then failed to parse.
use an explicit '|2' indentation indicator so the block is always read at
2 spaces regardless of the body. added a regression test that round-trips
an indented-first-line body through the yaml parser.
* chore: bump version to 0.12.5
* chore: begin 0.12.6.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
`validate` accepts a reject option case-insensitively
(`o.lower() in {"reject", "abort"}`), so a gate authored as
`options: [Approve, Reject]` passes validation. But `execute`
compared the echoed choice case-sensitively, so picking `Reject`
fell through to the approval path and silently ran downstream
steps instead of aborting.
Lower-case `choice` before the reject comparison so the runtime
agrees with the validation that let the option through.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_validate_remote_url in bundler/services/adapters.py guarded on parsed.netloc,
which is truthy for host-less URLs like "https://:8080" or "https://user@" even
though they carry no host. so those passed the "must be a valid URL with a host"
check. its docstring says it mirrors specify_cli.catalogs validation, but that
site was already fixed to use hostname in #3210/#3227 and this twin was missed.
switch to parsed.hostname (None for host-less URLs), matching catalogs.py. this
guard runs before any network call, so it is a pre-flight safety check.
add parametrized regression tests for the host-less forms plus a valid
host+port sanity case.
CatalogStack.search() claimed a bundle id in `seen` only when the entry matched
the query. so when the highest-precedence entry for an id did NOT match, a
lower-precedence entry with the same id could match and be returned instead --
even though resolve()/install always use the highest-precedence entry. search
advertised a bundle (name, version, source) the user could never actually get,
contradicting the method's own docstring ("resolved at its highest-precedence
source").
resolve every id to its highest-precedence entry first, then filter the
resolved set by the query. search now agrees with resolve(): a query that only
a shadowed lower-precedence copy matches returns nothing.
add a regression test covering the shadowed-entry case.
_safe_compare coerced both operands to int/float unconditionally for <, >, <=,
>=. any non-numeric string (an iso date, a version tag, a name) failed that
coercion and the whole comparison silently returned False -- so
`{{ inputs.d < '2026-02-01' }}` was False even when the date was earlier.
only coerce when both operands look numeric; otherwise compare the original
values, so two strings order lexicographically the way python does and two
numeric strings still compare as numbers ("10" > "9"). a number vs a
non-numeric string stays incomparable and yields False.
add a regression test covering dates, plain strings, numeric strings, and the
number-vs-string case.
* fix(workflows): quote-aware interpolation so a literal }} in a filter arg doesn't break multi-expression templates
#3208/#3228 hardened the single-expression fast path (_is_single_expression)
so a literal {{ or }} inside a string argument like `| default('}}')` stays on
the typed path. the multi-expression interpolation path was left on the old
_EXPR_PATTERN regex, whose non-greedy `(.+?)}}` body stops at the first }}
regardless of quoting. so a multi-expression template with a literal }} in any
block captured a truncated body, hit the filter parser malformed, and raised
ValueError.
e.g. `{{ inputs.name }}: {{ inputs.missing | default('}}') }}` raised instead
of interpolating.
replace _EXPR_PATTERN.sub with _interpolate_expressions, which scans each block
for a }} outside string literals - the same quote handling _is_single_expression
already uses. plain-value passthrough (a literal }} in a resolved value, not an
expression) is unchanged.
add regression tests for a literal }} in the second block and in the first
block, plus a literal {{ guard.
* fix(workflows): surface malformed templates in interpolation instead of emitting verbatim
address copilot review on #3307: when the quote-aware scan finds no block-closing
`}}` (e.g. an unbalanced quote in a filter arg swallowed the delimiter), fall back
to the first raw `}}` in the tail and evaluate it, so the parser raises ValueError
just as the old _EXPR_PATTERN.sub path did. only when there is no `}}` at all is
the tail left verbatim (a genuinely unterminated `{{`, which the regex also could
not match). keeps a typo failing loudly rather than being silently hidden.
add a regression test for an unbalanced quote in a multi-expression template.
* fix(integrations): cursor-agent ignores executable/extra-args env overrides
cursor-agent's build_exec_args() hardcoded self.key as argv[0] and never
called _apply_extra_args_env_var(), so the documented
SPECKIT_INTEGRATION_CURSOR_AGENT_EXECUTABLE (issue #2596) and
SPECKIT_INTEGRATION_CURSOR_AGENT_EXTRA_ARGS (issue #2595) hooks were
silently dropped — unlike every other CLI-dispatch integration (codex,
devin). Route argv[0] through _resolve_executable() and apply the
extra-args hook after the mandatory headless flags, mirroring the twins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(integrations): pin extra-args insertion order for cursor-agent
Per Copilot feedback: the extra-args override test only asserted the
injected tokens were present, not that they land before Spec Kit's
canonical --model / --output-format flags. Exercise build_exec_args with
both a model and JSON output and assert the extra args are inserted
before --model / --output-format (and the canonical flags stay intact and
paired). Verified this fails if the _apply_extra_args_env_var call is
moved after the flag extends.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs: drop stale kimi KIMI.md->AGENTS.md migration note
#3097 made the agent-context extension a full opt-in and removed the
KIMI.md -> AGENTS.md context migration from the kimi integration
(_migrate_legacy_kimi_context_file and the context_file handling are
gone). kimi's --migrate-legacy now only moves the skills directory. two
lines in the integrations reference still promised the removed context
migration; drop that clause so the docs match the code.
* docs: clarify kimi legacy migration is skill naming, not directory names
address review: the parenthetical said 'dotted->hyphenated directory
names', but the migration is about skill naming (speckit.xxx ->
speckit-xxx), matching the module docstring. reword to match.
* chore: bump version to 0.12.4
* chore: begin 0.12.5.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat(cli): add `py` script type & Python interpreter resolution (#3278)
Introduce a third script variant alongside `sh`/`ps` as the foundation
for unifying workflow scripts under a single Python implementation.
- Add `"py": "Python"` to `SCRIPT_TYPE_CHOICES`; `VALID_SCRIPT_TYPES`
consumers (init workflow step, init command, _helpers) pick it up
automatically since they derive from that mapping.
- Add `IntegrationBase.resolve_python_interpreter()` (project venv →
`python3` → `python`, falling back to `python3`).
- Prefix the resolved interpreter when `process_template()` expands
`{SCRIPT}` for the `py` script type so `.py` scripts run portably
(notably on Windows); thread `project_root` through callers so venv
preference works.
- Make `install_scripts()` mark copied `.py` files executable too.
Includes positive and negative unit tests for interpreter resolution,
`py` template processing, the new choice, and script installation.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(cli): return repo-relative venv interpreter & correct docstring
Address PR review feedback on #3285:
- `resolve_python_interpreter()` now returns the venv interpreter as a
path relative to the project root (`.venv/bin/python` /
`.venv/Scripts/python.exe`) instead of an absolute/joined path, so the
generated `{SCRIPT}` invocation stays portable and runnable from the
repo root regardless of where the project lives.
- Update `install_scripts()` docstring to note `.py` scripts are now
made executable alongside `.sh`.
- Update tests to assert the repo-relative interpreter path.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(cli): fall back to sys.executable for interpreter resolution
When neither python3 nor python is discoverable on PATH (and no project
venv is found), resolve_python_interpreter() now returns the running
interpreter (sys.executable) so the generated {SCRIPT} invocation works
in the current environment, falling back to "python3" only if that is
also unavailable. Update unit tests accordingly.
* fix(cli): quote py interpreter path when it contains whitespace
For the `py` script type, the resolved interpreter may be an absolute
path containing spaces (notably `sys.executable` under Windows
`Program Files`). Quote it when it contains whitespace so the `{SCRIPT}`
invocation isn't split into multiple arguments. Add positive/negative
tests for the quoting behavior.
* test: guard executable-bit assertions from Windows chmod semantics
The Windows CI job failed because `os.chmod` does not set POSIX
executable bits on Windows, so `install_scripts()` cannot make `.py`/
`.sh` files executable there (nor is it needed — the interpreter is
invoked explicitly). Split the install_scripts test so file-copy
behavior is still verified cross-platform, and skip the executable-bit
assertions on win32 (matching the repo's existing pattern).
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: resolve GitHub release asset API URL for private repo bundle downloads
For private/SSO-protected GitHub repos, browser release download URLs
(https://github.com/<owner>/<repo>/releases/download/<tag>/<asset>)
redirect to an HTML/SSO page instead of delivering the asset, causing
bundle manifest downloads to fail.
Extends the pattern from #2855 (presets/workflows) to cover the bundle
manifest download path in _download_remote_manifest:
- Resolves browser release URLs to GitHub REST API asset URLs via
resolve_github_release_asset_api_url before downloading
- Direct REST API asset URLs (api.github.com/repos/.../releases/assets/<id>)
are passed through directly
- Both cases use Accept: application/octet-stream so the API returns the
binary payload rather than JSON metadata
- The original catalog URL is used to determine artifact format (.zip vs
YAML) since the resolved API URL does not carry the file extension
Adds two CLI-level contract tests:
- bundle info resolves browser release URL via GitHub tags API
- bundle info passes direct API asset URL through with octet-stream
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: detect ZIP payload by magic bytes; add zip and API-asset tests
Address Copilot review feedback on PR #3136:
1. Detect ZIP payloads by magic bytes (PK\x03\x04) in addition to the
'.zip' URL suffix so that direct GitHub REST asset URLs — which carry
no file extension — are correctly routed through the ZIP extraction
path when the asset is a ZIP bundle artifact.
2. Add two new contract tests:
- test_bundle_info_resolves_github_browser_release_url_zip: exercises
the '.zip' browser release URL path end-to-end, verifying the tags
API lookup fires, octet-stream header is used, and bundle.yml is
successfully extracted from the ZIP payload.
- test_bundle_info_api_asset_url_zip_detected_by_magic_bytes: verifies
that a direct REST asset URL returning ZIP bytes is detected by magic
and parsed correctly without a tags API call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: improve error message, broaden ZIP magic, drop unused tmp_path
Address second-round Copilot review feedback on PR #3136:
- Error message: when the download fails, report the original catalog
download_url so the user knows which entry to fix; include the resolved
REST API URL when it differs for easier debugging.
- ZIP detection: broaden the magic-bytes check from PK\x03\x04 to raw[:2]
== b"PK", covering all valid ZIP variants (local-file header PK\x03\x04,
empty-archive PK\x05\x06, spanned/split PK\x07\x08).
- Tests: remove the unused tmp_path parameter from
test_bundle_info_resolves_github_browser_release_url_zip.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: use full 4-byte ZIP signatures instead of 2-byte PK prefix
Address Copilot feedback: raw[:2] == b"PK" is too broad and could
misclassify any payload starting with ASCII "PK" as a ZIP, producing
a confusing "not a valid bundle" error.
Use the three specific 4-byte ZIP magic signatures instead:
PK\x03\x04 — local file header (standard ZIP)
PK\x05\x06 — end-of-central-directory (empty archive)
PK\x07\x08 — data descriptor / spanning marker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: harden _download_remote_manifest parsing and tighten tests
- Promote _ZIP_SIGNATURES to module-level constant (was redefined per call)
- Use PurePosixPath for URL path suffix extraction so query strings and
fragments are ignored and URL paths are treated as POSIX on all OSes
- Move yaml/BundleManifest imports to function top to flatten the
previously nested try/except into a single handler with explicit
except _yaml.YAMLError and except Exception clauses
- Re-add None guard on _local_manifest_source return: the function is
typed Optional[BundleManifest] and without the guard a None return
propagates silently to callers that degrade gracefully rather than
raising an actionable error; comment explains it is defensive not dead
- Assert exact resolved asset URL in browser-URL download tests, not
just the Accept header, so a regression where download uses the
original URL instead of the resolved one would be caught
- Add resolution-failure test: when tags API finds no matching asset the
code falls back to the original URL and exits non-zero with Error:
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(bundle): pass github_provider_hosts() for GHES private release downloads
Extends the GHES support pattern from extensions and presets (#2855, #3157)
to the bundle manifest download path: resolve_github_release_asset_api_url
now receives github_hosts=github_provider_hosts() so browser release URLs
from GitHub Enterprise Server instances are resolved via /api/v3 rather
than falling back to the unauthenticated download path.
Also adds a contract test covering the GHES resolution path for
_download_remote_manifest (analogous to the existing github.com tests).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(bundle): remove unused ghes_entry variable from GHES contract test
The dict was defined but never consumed — the test drives GHES host
recognition entirely through the github_provider_hosts() patch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(bundle): include source URL in remote manifest parse errors
Thread the catalog URL (and resolved API URL when it differs) into the
YAML parse, generic parse, and ZIP-extraction error paths of
_download_remote_manifest so failures point at the offending source
instead of an opaque temp path. Addresses PR review feedback.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add Analytics extension to community catalog
Add analytics extension submitted by @Huljo to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table
Closes#3288
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix empty changelog field for analytics extension
Set the analytics extension changelog to the GitHub releases page instead of
an empty string, which the catalog treats as a URI when present and can fail
schema validation and downstream tooling.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
* fix: interpolate multi-expression templates instead of returning None (#3208)
`evaluate_expression` returned None for templates containing two or more
`{{ }}` blocks with no surrounding literal text, e.g.
`"{{ context.run_id }} {{ inputs.issue }}"`.
The single-expression fast path used `_EXPR_PATTERN.fullmatch()`, but
`fullmatch` defeats the pattern's non-greedy `(.+?)` body: for two adjacent
expressions it still matches, capturing everything between the first `{{`
and the last `}}` (`"context.run_id }} {{ inputs.issue"`) as the body. That
garbage failed dot-path resolution and returned None directly, bypassing the
`sub()` interpolation path that would have resolved each expression. Downstream
this surfaced as the literal string "None" reaching commands.
Guard the fast path on `stripped.count("{{") == 1` so only genuine
single-expression templates take the typed return; multi-expression templates
fall through to `sub()` and interpolate correctly.
Add regression tests for two expressions separated by a space and for adjacent
expressions with no separator.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(expressions): use match-span guard so single expressions with literal {{ keep their type
The previous `stripped.count("{{") == 1` guard misclassified a genuine
single expression whose string argument contains a literal `{{` (e.g.
`{{ inputs.text | contains('{{') }}`) as multi-expression, routing it
through `sub()` interpolation and coercing the typed (bool/int/list)
return value to a string -- breaking the type-preservation the docstring
promises (Copilot review on #3228).
Anchor a single match at the start and require it to consume the whole
stripped string instead. The non-greedy body stops at the first `}}`, so
a two-block template fails the span check (falls through to interpolation,
fixing #3208) while a lone expression -- including one with a `{{` inside
a string literal -- matches to the end and keeps its typed value.
Add a regression test for the literal-brace single-expression case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(expressions): detect single expression with quote-aware scan
The match-span guard using the non-greedy _EXPR_PATTERN stopped at the
first `}}`, so a lone expression whose string argument contains a literal
`}}` (e.g. `{{ inputs.text | contains('}}') }}`) was misclassified as
multi-expression and mis-parsed by the interpolation path, raising
ValueError and turning CI red (Copilot review on #3228).
Replace the span check with `_is_single_expression`, which scans the
`{{ ... }}` body for a block-closing `}}` outside string literals (mirrors
the quote handling already in `_split_top_level_commas`). A genuine
two-block template closes early and falls through to interpolation
(fixing #3208); a lone expression with a literal `{{` or `}}` inside a
string argument keeps its typed return value.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* feat(cli): honor SPECIFY_INIT_DIR in the specify CLI project resolver
The shell resolver honors SPECIFY_INIT_DIR (#2892), but the Python CLI did
not: it resolved the project as Path.cwd() + a .specify/ check and never read
the override. So setup-plan.sh respected it while `specify integration install`
ignored it, and you still had to cd into the member project.
Route project resolution through a shared _resolve_init_dir_override() that
applies the shell resolver's validation rules (relative to cwd, must exist and
contain .specify/, hard error, no fallback, same error strings). It's wired into
_require_specify_project() — the chokepoint for every project-scoped subcommand
(integration/extension/workflow/preset/...) — and the `workflow run <file>`
standalone path, which re-applies its symlinked-.specify guard on the override
branch too. init is unchanged: it creates .specify/, so the must-pre-exist rule
doesn't apply.
The resolver canonicalizes symlinks via Path.resolve() while the shell keeps the
logical path; they agree for non-symlinked paths (documented in the resolver).
Tests in tests/test_init_dir_cli.py mirror the strict cases from test_init_dir.py
through the CLI; conftest now strips SPECIFY_* for the whole suite so a stray
export can't perturb the now-env-reading resolver. Docs note the CLI applies the
same rules.
Discussion: github/spec-kit#2834
(Disclosure: I used an AI coding agent to audit the call sites and resolver,
draft the change, and run an adversarial code review; reviewed by me.)
* fix(cli): honor SPECIFY_INIT_DIR for bundle commands
Assisted-by: Codex (model: GPT-5, autonomous)
* fix(bundler): refuse symlinked .specify on the SPECIFY_INIT_DIR override path
find_project_root refuses a symlinked .specify (following it could read/write
outside the tree, and a test pins that), but the SPECIFY_INIT_DIR override added
for bundle commands returned early and skipped that guard:
_resolve_init_dir_override validates .specify with is_dir(), which follows
symlinks. So `specify bundle` accepted via the override a layout the cwd path
rejects. Re-check the override result with the same guard, plus a regression test.
(Disclosure: found via an AI code review and fixed with an AI coding agent;
reviewed by me.)
* fix(cli): keep SPECIFY_INIT_DIR strict for bundles
Treat an explicit symlinked SPECIFY_INIT_DIR project as a hard bundle error instead of returning no project, which could initialize the current directory. Align the docs with the actual unset resolver behavior.
Assisted-by: Codex (model: GPT-5, autonomous)
* docs(core): note symlinked .specify handling differs across CLI surfaces
A symlinked .specify is followed by integration/extension/workflow (matching the
shell resolver) but refused by bundle and workflow run <file> (write
confinement). Document the asymmetry so it reads as intentional.
(Disclosure: AI-assisted; reviewed by me.)
* docs(core): reframe symlinked .specify note around the override invariant
Per maintainer feedback on #3186: SPECIFY_INIT_DIR relocates where the project
is, not how a surface treats symlinks. Each surface keeps its cwd-path stance
(write surfaces refuse a symlinked .specify, read/config surfaces follow it),
so the split is one policy relocated, not an inconsistency.
* docs: address Copilot review on resolver docstrings
- _project.py: the error messages "mirror" the shell wording rather than
"match" it (the CLI renders a Rich `Error:` line, the shell a plain `ERROR:`).
- find_project_root: document that honoring SPECIFY_INIT_DIR when start is None
can raise typer.Exit / BundlerError, so the Path | None signature isn't
surprising to direct callers.
* docs(bundler): note require_project_root inherits the override raise behavior
find_project_root can raise typer.Exit / BundlerError under the SPECIFY_INIT_DIR
override (start=None); require_project_root inherits that, so document it
alongside its own BundlerError-on-missing-project.
* docs: clarify symlinked project root behavior
Assisted-by: OpenAI Codex (model: GPT-5, autonomous)
* Address SPECIFY_INIT_DIR review feedback
Assisted-by: OpenAI Codex (model: GPT-5, autonomous)
* Route workflow JSON errors to stderr
Assisted-by: OpenAI Codex (model: GPT-5, autonomous)
`_load_core_command_names()` computed its candidate command dirs with
bespoke `Path(__file__)` arithmetic. The #3014 move of this module from
`specify_cli/extensions.py` to `specify_cli/extensions/__init__.py`
pushed the file one directory deeper but left the `.parent` counts
unchanged, so both candidates resolved to non-existent paths:
wheel -> specify_cli/extensions/core_pack/commands (real: specify_cli/core_pack/commands)
source -> src/templates/commands (real: repo-root templates/commands)
Neither exists, so every call silently fell through to
`_FALLBACK_CORE_COMMAND_NAMES`. Discovery is latent-dead: the fallback
happens to equal the real stems today, but the shadowing guard (#1994)
that depends on it now relies on someone hand-editing the fallback on
every core-command add/remove (as already happened for `converge`, #3001).
Delegate path resolution to the canonical `_locate_core_pack` /
`_repo_root` resolvers in `_assets` — the same ones the presets and
bundle loaders use. They are anchored to the package root, so discovery
survives future module moves.
Add regression tests that point the resolvers at a temp tree with
*different* command names, proving discovery reads from disk rather than
returning the fallback (they fail on the pre-fix code).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: fall back to feature dir basename for empty CURRENT_BRANCH (#3026)
When a feature is resolved via SPECIFY_FEATURE_DIRECTORY or .specify/feature.json
without SPECIFY_FEATURE set, get_current_branch() returns empty, so
get_feature_paths / Get-FeaturePathsEnv emitted CURRENT_BRANCH= (empty) even
though the feature directory was resolvable. Downstream scripts and agents that
expect a non-empty identifier got misleading output.
Fall back to the basename of the resolved feature directory when the branch is
empty, in both the bash (`${feature_dir##*/}`) and PowerShell
(`Split-Path -Leaf`) resolvers. An explicit SPECIFY_FEATURE still takes
precedence, so this only fills the previously-empty case.
Add bash + PowerShell regression tests: the basename fallback fires when
SPECIFY_FEATURE is unset, and an explicit SPECIFY_FEATURE still overrides it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix: address Copilot feedback — PS 5.1 compat + parametrize bash test
- common.ps1: replace [System.IO.Path]::TrimEndingDirectorySeparator
(a .NET Core-only method that throws MethodNotFound on Windows
PowerShell 5.1 / .NET Framework) with a portable String.TrimEnd,
so the trailing-slash trim actually works on 5.1.
- tests: parametrize the bash fallback test to cover feature.json,
SPECIFY_FEATURE_DIRECTORY, and the explicit SPECIFY_FEATURE override
(mirrors the PowerShell test), folding in the old explicit-override
test; add the missing blank line before the next test (PEP 8).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* feat(bug-fix): add label-driven bug-fix agentic workflow
Add a `bug-fix` gh-aw workflow as stage 2 of the assess -> fix -> test
bug pipeline, mirroring the existing `bug-assess` stage. It triggers when
a maintainer applies the `bug-fix` label, recovers the slug and remediation
contract from the prior bug-assess assessment comment, applies the fix, and
opens a draft pull request plus a summary comment for human review.
The workflow is intentionally decoupled from Spec Kit specifics: it consumes
the assessment from the issue comment rather than any `.specify/` files, so it
is portable to other repositories running the matching bug-assess stage.
- .github/workflows/bug-fix.md authored and compiled to bug-fix.lock.yml
- Label-gated trigger (github.event.label.name == 'bug-fix')
- Draft PR via create-pull-request safe-output; scoped permissions
- Untrusted-input / URL-safety guardrails consistent with bug-assess
- Maintainer remains the gatekeeper; no unattended automation
Refs #3238
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(bug-fix): tighten bash allowlist and block protected files
Address Copilot review feedback on PR #3258:
- Trim tools.bash to the inspect set plus a small test-runner set
(pytest, npm, go, cargo, dotnet), dropping package-manager/build
tools (pip, npx, pnpm, yarn, mvn, gradle, make, bundle, rake, ruby,
node) to reduce blast radius under prompt injection.
- Set create-pull-request.protected-files.policy: blocked so edits to
sensitive files (dependency manifests, README/CHANGELOG/SECURITY,
etc.) block PR creation, matching the stronger contract used by the
other PR-creating workflows in this repo.
Refs #3238
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(bug-fix): resync lock body_hash after review edits
The Copilot autofix commits edited bug-fix.md (verdict phrasing, Assisted-by
trailer) but did not recompile the lock, leaving body_hash stale. Since the
workflow runs with strict integrity, the runtime-imported bug-fix.md must match
the lock's recorded body_hash. Recompiled with gh-aw v0.79.8 (checkout pin kept
at v7.0.0 to match sibling locks); the only change is the body_hash.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(bug-fix): align add-labels max to 1 and soften next-stage label reference
Address two Copilot review findings:
- add-labels.max: the authored frontmatter said max:1 but the committed lock
enforced max:2 (stale from an earlier frontmatter), and Step 8 said 'max 2
labels total'. The workflow only ever applies ONE status label per run
(fix-proposed | needs-reproduction | fix-blocked | needs-assessment), so 1 is
the correct, tightest contract. Recompiled so the lock now enforces max:1, and
reworded Step 8 to 'exactly one status label per run'.
- bug-test label: Step 7 hard-coded applying a 'bug-test' label that does not
exist in this repo. Since the workflow is portable, reworded to present the
stage-3 bug-test workflow as the planned next stage 'if the repository has it
configured' rather than assuming it exists.
Recompiled with gh-aw v0.79.8; checkout pins kept at v7.0.0 to match sibling
locks. No compile drift.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(bug-fix): set add-labels max to 1 consistently across source and lock
A prior autofix flipped the authored frontmatter add-labels.max back to 2,
re-introducing the mismatch: source said 2, the compiled lock enforced 1, and
Step 8 prose says 'exactly one status label per run'. The workflow only ever
applies a single status label per run (needs-assessment | needs-reproduction |
fix-proposed | fix-blocked), so 1 is the correct, tightest contract and matches
the compiled lock. Set the frontmatter to max:1 so source, lock, and prose all
agree (also avoids the lock staleness guard failing on a frontmatter mismatch).
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(bug-fix): relax protected files and number bug-fix branches
Address the two new Copilot review findings:
- was still covering
README.md and CHANGELOG.md, which can legitimately need updates as part of a
prior bug remediation. Add them to the exclude list so the workflow can still
open a PR when the assessment calls for documentation changes, matching the
pattern used by add-community-extension.
- The generated branch name used , but the repo
convention for bug fixes requires so branches are
traceable and aligned with AGENTS.md. Update the branch naming guidance to use
.
Recompiled with gh-aw v0.79.8; lock reflects the protected-files exclusion and
keeps the v7.0.0 checkout pin fixups.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(bug-fix): accept workflow-authored assessment comments from bot/service accounts
Address the open Copilot finding on assessment-author matching.
The workflow previously required the prior assessment comment to be authored by
`github-actions[bot]`. That is too strict for portable repos where bug-assess
may post through a different bot/service account token.
Updated Step 1 to select the most recent assessment comment that appears
workflow-authored by combining:
- bot/service-account authorship, and
- expected bug-assess structure (assessment header plus remediation/files/tests sections).
This keeps the spoof-resistance intent while removing dependence on one fixed
login.
Recompiled with gh-aw v0.79.8 and kept checkout v7.0.0 pin fixups.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(bug-fix): clarify local-check guardrails for dependency fetching
Address Copilot feedback on Step 5 consistency around network-dependent checks.
The workflow previously listed `go test ./...` and `cargo test` as examples
while also forbidding network-dependent commands, which could be ambiguous on
clean runners.
Updated Step 5 to:
- keep those commands as examples only when dependencies are already present
- explicitly disallow dependency-fetch/install commands during verification
(go mod download/go get/cargo fetch/npm|pnpm|yarn install)
Recompiled with gh-aw v0.79.8 and kept checkout v7.0.0 pin fixups.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(bug-fix): make status label application conditional on label existence
Address Copilot feedback about missing status labels causing runtime failures.
The workflow previously instructed unconditional application of
`needs-assessment`, `fix-blocked`, and `fix-proposed`. In repositories where
those labels are not pre-created, `add_labels` fails and can break the run.
Updated Steps 1/3/4/8 to require existence checks before adding those labels:
- add the label only if it exists
- otherwise skip labeling and explicitly note that in the comment
This preserves the status-label UX when labels exist while keeping execution
robust in repos that have not created every optional status label yet.
Recompiled with gh-aw v0.79.8 and kept checkout v7.0.0 pin fixups.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
* feat(workflows): add label-driven bug-test workflow (#3239)
Add the third stage (assess → fix → test) of the semi-automated, human-gated
bug pipeline. The `bug-test` agentic workflow triggers when a maintainer applies
the `bug-test` label, runs the relevant tests in isolation against the fix,
compiles a readable pass/fail report, and posts it back as a single issue
comment.
- Locates the fix under test: linked PR → named fix branch → current checkout
fallback, only ever from origin.
- Stack-agnostic test detection (uv+pytest, npm/pnpm/yarn, go, make) so it is
decoupled from Spec Kit specifics and reusable by other projects.
- Runs tests under a timeout as untrusted code; scoped read-only permissions;
same URL-safety / untrusted-input guardrails as bug-assess.
- Verification mode compares a generated fix against the historical fix for
old/closed bugs to surface discrepancies.
- Optional single result label (tests-passing / tests-failing /
tests-inconclusive).
Compiled bug-test.lock.yml with `gh aw compile`.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(workflows): bump actions/checkout from 6.0.3 to 7.0.0 in bug-test workflow
Align with repo standards (e.g. dependabot PR #3064, other workflows).
Manually pinned in the compiled lock file for consistency.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* chore: bump version to 0.12.3
* chore: begin 0.12.4.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat(copilot): default to skills mode
* feat(copilot): warn before skills default rollout
* Make Copilot skills warning test less brittle
---------
Co-authored-by: root <kinsonnee@gmail.com>
docs/reference/bundles.md and docs/reference/authentication.md exist on
disk but were absent from the Reference section of docs/toc.yml, so both
pages were orphaned and undiscoverable in the published docs sidebar.
Add the two nav entries (Bundles after Workflows, matching the ordering
in reference/overview.md; Authentication last).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
zed is registered, registrar-aligned and registry-tested, but it was the
only one of the 34 integrations absent from integrations/catalog.json,
making it undiscoverable through the discovery manifest. Add the missing
'zed' entry (mirroring the sibling skills entries) and a registry<->catalog
parity regression test so a future integration can't silently drift.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The hook-note injection regex matches the line terminator via
(\r\n|\n|$), so the captured eol group is empty when the instruction
is the final line of a file with no trailing newline. The cline
integration emitted the note with that empty eol, mashing the note text
and the instruction onto a single line. Default eol to '\n', matching
the agy integration twin which already guards this case.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* refactor: move workflow command handlers to workflows/_commands.py (PR-8/8)
Final PR of the __init__.py split. Moves the workflow command group out of
__init__.py into the existing workflows/ package, completing the domain-dir
layout established in PR-5 (integrations), PR-6 (presets) and PR-7
(extensions).
- New workflows/_commands.py holds the four Typer apps (workflow / catalog /
step / step-catalog), all 25 command handlers, the six workflow-only
helpers (_parse_input_values, _workflow_run_payload, _emit_workflow_json,
_stdout_to_stderr_when, _validate_step_id_or_exit,
_resolve_steps_base_dir_or_exit), and a register(app) entry point.
- workflows is already a package, so no rename is needed; intra-package
imports change from `.workflows.x` to `.x`. The only root-helper dep
(_require_specify_project) is reached through a call-time shim so test
monkeypatching of specify_cli._require_specify_project keeps working.
- __init__.py drops ~1445 lines (2066 -> 621); the workflow group is
re-attached via register(app). Dead `contextlib` import removed.
- tests/test_workflows.py: import the now-relocated _stdout_to_stderr_when
helper from its new home (workflows._commands) instead of the package root.
No behavior change. Full suite green (3847 passed), ruff clean.
* Prevent workflow state writes through symlinked storage
Workflow commands persist run state under .specify/workflows/runs, so the command-local project shim now rejects symlinked workflow storage before any workflow command proceeds. The standalone YAML path uses the same guard because it intentionally bypasses the normal project requirement while still creating workflow state under the current directory.
Constraint: Local YAML workflow runs do not require an existing .specify project directory but still create .specify/workflows/runs state
Rejected: Guard only .specify in the file-source path | .specify/workflows and runs can independently redirect writes
Confidence: high
Scope-risk: narrow
Directive: Keep workflow storage symlink checks centralized before constructing WorkflowEngine
Tested: .venv/bin/python -m pytest tests/test_workflow_run_without_project.py tests/test_workflows.py::TestWorkflowAddSymlinkGuard -v
Tested: .venv/bin/python -m py_compile src/specify_cli/workflows/_commands.py tests/test_workflow_run_without_project.py tests/test_workflows.py
Not-tested: Ruff lint; ruff is not installed in the repo virtualenv
Assisted-by: OpenAI Codex (model: GPT-5, autonomous)
* fix(workflows): pass github_hosts allowlist to GHES release asset resolver
workflow add resolved GitHub release download URLs without forwarding the
github_provider_hosts() allowlist, so resolve_github_release_asset_api_url
never treated any host as GHES. This regressed GitHub Enterprise Server
release asset resolution and diverged from presets/extensions, which already
pass github_hosts. Forward github_provider_hosts() at both the direct-URL and
catalog install call sites. The allowlist remains the anti-SSRF gate.
* fix(workflows): reject symlinked/traversal <id> dir on workflow install
Local/URL and catalog installs wrote to .specify/workflows/<id>/workflow.yml
without guarding the <id> segment. A pre-planted symlink at <id> or
<id>/workflow.yml let mkdir+copy/download follow it and write outside the
project root; a non-directory <id> made mkdir raise unhandled.
Add _safe_workflow_id_dir() to reject path traversal, symlinked or
non-directory <id>, and a symlinked workflow.yml leaf before any write.
Fold the catalog branch's existing traversal check into the helper.
* fix(workflows): harden _safe_workflow_id_dir output and leaf checks
- Reorder symlink/non-directory check before resolve() so a symlinked
<id> reports as symlinked instead of misleading "Invalid workflow ID"
- Reject a pre-existing <id>/workflow.yml that is not a file, avoiding an
unhandled IsADirectoryError on later write/copy2
- Escape workflow_id in Rich output to prevent markup injection; escape
the repr (not the raw id) so repr-added backslashes cannot re-expose
brackets, matching extensions/_commands.py hardening
- Add tests for workflow.yml-as-directory and markup-escaped invalid id
* Avoid stale lint failures from config helper imports
Move PyYAML loading into the helpers that read and write agent-context configuration, and replace the broad Any annotation with object. The runtime behavior stays the same while the module no longer exposes top-level imports that can be flagged as unused when CI analyzes a narrower code shape.
* Prevent workflow commands from targeting reserved storage
Workflow install and removal paths are derived from workflow IDs before any catalog download, local copy, or directory deletion. Validate that IDs are single workflow-id path segments and reject names reserved for workflow runtime storage so commands cannot target .specify/workflows/runs or .specify/workflows/steps.
* chore: retire roo integration — extension shut down (#3167)
Remove the Roo Code integration after the extension was shut down: subpackage,
registry entry, catalog entry, docs, tests, and issue-template options.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: remove stale Roo Code mention in upgrade guide
Assisted-by: GitHub Copilot (model: gpt-5.3-codex, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: remove leftover Roo Code references after merge
Drop roo from presets/ARCHITECTURE.md example and the agent-context
defaults map; these came in from main and were flagged by review.
Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundle): allow 'catalog remove' by the same relative path used to add
add_source canonicalizes a local catalog path to an absolute url before persisting it, but remove_source compared only the raw input against the stored id/url. So 'bundle catalog remove ./cat.json' could not undo 'bundle catalog add ./cat.json' -- the stored url was absolute, the removal target relative, and they never matched ('No project-scoped catalog source found'). Match the canonicalized form too (a no-op for ids and remote urls), so a local source is removable by the same path it was added with.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(bundle): match catalog removal target exactly first, canonical only as fallback
Address Copilot review: canonicalizing the removal target unconditionally could let 'remove <id>' also delete a different source whose url equals that id's canonicalized path (ids are treated as local paths by _canonicalize_url, empty scheme). Try an exact id/url match first; only fall back to a canonicalized-url match when no exact match is found, so relative-path removal still works without collateral deletion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): reject bool max_iterations in while/do-while validation
while/do-while validate() checked 'not isinstance(max_iter, int) or max_iter < 1'. Since bool is a subclass of int, isinstance(True, int) is True and True < 1 is False, so 'max_iterations: true' passed validation and then ran as a single iteration (range(True) == range(1)) instead of being reported as a type error. Reject bools explicitly, matching the fail-fast-on-bool handling already used for number inputs and gate options.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: assert empty error list for the valid do-while max_iterations case
Address Copilot review: the accepted-config assertion only checked that no error mentioned 'max_iterations', which could let an unrelated validation error pass unnoticed. For a known-good config, assert the entire error list is empty (consistent with the other validate tests in this file).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: generate integrations reference from catalog
* refactor: integrate table rendering into specify integration search --markdown
- Remove standalone scripts/generate_integrations_reference.py
- Strip doc injection machinery from catalog_docs.py; keep only table rendering
- Wire render_integrations_table() into existing --markdown flag of integration search
- Remove old simple markdown table block from integration_search (was Name|ID|Version|Description|Author)
- Simplify tests: drop subprocess/doc-path tests, keep table rendering and metadata tests
- Clean up docs/reference/integrations.md: remove generated markers, update note
* fix: address Copilot review feedback on catalog_docs and integration_search
- Warn when --markdown is combined with filters (query/--tag/--author) which are
silently ignored; catch ValueError/FileNotFoundError and surface clean error
via console instead of raw traceback (r3244821516)
- Add coverage enforcement in list_integrations_for_docs(): raises ValueError
with actionable message if any registry key is missing from INTEGRATION_DOC_URLS,
preventing silently incomplete doc tables (r3244821589)
- Rename test to accurately reflect sources: label derives from registry config,
URL comes from INTEGRATION_DOC_URLS doc map — not solely from registry (r3244821607)
- Simplify test dict construction to idiomatic dict comprehension (r3244821619)
* fix: add sync test, INTEGRATIONS_REFERENCE_PATH constant, and fix naming
* revert: restore docs/reference/integrations.md to upstream/main; remove sync test (GH Actions job will handle)
* fix: remove dead INTEGRATIONS_REFERENCE_PATH, drop URL-length padding, fix docstring, drop FileNotFoundError
* fix: send --markdown warnings/errors to stderr, rename test for clarity
* fix: detect stale doc-map keys, test _render_cell escaping, strengthen header assertion
* refactor: promote _render_cell to public render_cell function
* test: mock registry and doc maps to avoid brittle live registry coupling
* refactor: flatten patches, remove unused imports, fix trailing whitespace, optimize missing calculation
* refactor: make validation non-fatal, fix context manager syntax, add CLI tests
* fix: improve docstring clarity, test robustness, and exception handling
* fix: improve test assertions, disable warnings by default, enhance exception handling
* fix: make CLI tests deterministic and improve config access resilience
* fix: remove extra blank line, add stale keys validation, add regression test for docs sync
* Fix 5 remaining feedback items:
- Rename _get_mocked_cli_runner() to _get_catalog_docs_patches() for clarity
- Use ExitStack context manager for guaranteed patch cleanup
- Add explicit UTF-8 encoding to file reads
- Skip doc sync test gracefully when docs aren't present
- Remove exception chaining from typer.Exit to avoid noisy tracebacks
* address all outstanding copilot review feedback on PR 2563
* Address Copilot feedback: escape URLs in markdown links, deduplicate cell rendering, fix table parser for escaped pipes
* Address 3 new Copilot feedback: add URL escaping test, fix parse_first_markdown_table for escaped pipes, guard community tests with skip
* Address 3 new Copilot feedback: escape id field, remove unused alias, escape integration URLs
* Address 3 new Copilot feedback: fix comment name, include all integrations in list
* Fix architectural issue: escape raw fields before composing Markdown to prevent double-escaping
* Deduplicate _escape_url_for_markdown_link and add URL escaping test
* Address 4 new Copilot feedback: add trailing newline, fix test helper ExitStack, update warning message
* Address 4 new Copilot feedback: make escape function public, fix error message, validate test rows, prevent double newline
* Update error message in test_missing_catalog_file for clarity
* Remove obsolete integrations sync test
* keep integrations docs in sync
* fix: allow prerelease spec-kit versions in compatibility checks
Allow prerelease/dev builds to satisfy extension and preset compatibility
checks when their version number falls within the required specifier range.
Also harden the integrations docs rendering helpers and add regression
coverage for the markdown table parsing and version gating paths.
Tests: pytest -q; python3 -m compileall -q .; black/flake8 unavailable
Reference: branch 002-generate-integrations-docs; source patch /tmp/spec-kit-changes.patch
* fix: isolate prerelease compatibility gate changes
Keep the prerelease/version compatibility fix on its own branch and remove
the unrelated integrations docs updates that belong with PR 2563.
Tests: full suite passed on the prerelease branch before splitting; docs branch covered by targeted docs tests
Reference: upstream/main; source patch /tmp/spec-kit-changes.patch
* Address PR 2695 feedback: Centralize prerelease policy and add boundary test
* Address remaining Copilot PR feedback: revert docs and add preset prerelease tests
* Remove unreachable raise CompatibilityError
* Fix PEP8 E302 and E303 formatting issues
* chore: bump version to 0.12.2
* chore: begin 0.12.3.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(scripts): portable uppercase for branch-name acronym retention
Branch-name generation keeps short uppercase acronyms (e.g. "AI") by re-checking
the lowercased word against the original description with ${word^^}. That
parameter expansion is bash 4+ only; on macOS's default bash 3.2 it errors with
"bad substitution", so the acronym/short-word retention branch never matches and
those words are dropped ("go AI now" yields 001-now instead of 001-ai-now). Use
tr '[:lower:]' '[:upper:]' instead, which is portable.
Applies to both the core create-new-feature.sh and the git extension's
create-new-feature-branch.sh. The existing
test_branch_name_short_word_case_sensitivity / test_short_word_retention tests
cover this and now pass on bash 3.2 (CI runs on bash 4+/Linux, so they passed
there already).
(Disclosure: an AI coding agent surfaced the failure while running the suite on
macOS and pinned the root cause; fix written and reviewed by me.)
* fix(scripts): portability follow-ups from code review
- core create-new-feature.sh: match the acronym with `grep -qw` (POSIX
whole-word) instead of `\b...\b` (GNU/BSD-only), matching the git extension
and dropping a non-POSIX construct.
- lint: add a CI guard rejecting bash 4+ case-modification expansions in *.sh.
shellcheck assumes bash 4+ from the shebang and can't flag them, and CI has no
bash-3.2 lane, so this prevents silently re-shipping the macOS regression this
PR fixes.
- update a stale PowerShell extension comment that cited the removed bash idiom.
(Disclosure: prompted by an AI code review of the PR; written and reviewed by me.)
* feat(workflows): honor max_concurrency in fan-out via a bounded thread pool
* feat(workflows): address review — sliding-window fan-out, locked output, faithful halt
Address the reviewer feedback on the bounded fan-out concurrency:
- Sliding submission window: keep at most `workers` items in flight and stop
launching new items once the run is halting, instead of submitting all items
up front (which let the pool keep starting queued work after a halt).
- Faithful halt prefix: attribute a halt to the specific item whose own
recorded result halted the run (replaying the sequential break condition,
honoring continue_on_error/aborted), not the shared run status a later
concurrent item may have flipped. The returned prefix now includes the actual
halting item, matching the sequential path. An item that fails before
recording a result (e.g. an unknown step type) is attributed too, since every
item runs the same template.
- Lock the parent fan-out output mutation: route the post-fan-out
step_results[...]['output'] update through a new RunState.set_step_output()
under the run lock, so it cannot race a concurrent save().
- Docstring: describe int() coercion accurately (numeric strings / floats are
honored; only non-coercible or <= 1 runs sequentially).
Tests: add concurrent halt-includes-halting-item, continue_on_error-does-not-
truncate, and unknown-template-type-matches-sequential coverage; make the
timing test use a monotonic clock with a looser threshold to avoid CI flakiness.
* feat(workflows): address second review pass — concurrency hardening
- append_log: serialize the log_entries append + log.jsonl write under a
dedicated RunState._log_lock so concurrent fan-out workers can't interleave
or corrupt log lines (kept separate from the state lock; never nested).
- _run_fan_out.run_item: read the item output back through the item_ctx it
executed against rather than the outer context closure — clearer and robust
if StepContext ever stops sharing the steps dict by reference.
- StepBase: document the thread-safety contract — STEP_REGISTRY holds one shared
instance per type, so concurrent fan-out invokes execute() on the same object;
implementations must be stateless/thread-safe (the built-ins already are).
- test_concurrency_is_real: prove parallelism deterministically with a
threading.Barrier (sequential execution can't clear it) instead of a
wall-clock timing assertion.
* feat(workflows): address review — stamp updated_at under lock, clarify cancel semantics
- RunState.save(): move the updated_at timestamp assignment inside the run lock
so the timestamp matches the snapshot the thread serializes and concurrent
savers don't race on it.
- _run_fan_out docstring: clarify that on a halt only not-yet-started items are
cancelled; items already running finish but their outputs are ignored
(Future.cancel() can't stop running work, and the pool joins on exit).
* feat(workflows): serialize on_step_start callback under a lock
The concurrent fan-out path invokes _execute_steps from worker threads, which
calls the engine's on_step_start callback (the CLI sets it to a console.print
lambda). Concurrent invocation could interleave/garble progress output. Guard
the call with a WorkflowEngine._callback_lock so callbacks are serialized;
the lock is uncontended for sequential runs.
* feat(workflows): re-raise worker exceptions in-place to preserve traceback
In _run_fan_out's concurrent path, a worker exception was stashed in first_exc
and re-raised after the loop. Re-raise it from within the except block with a
bare `raise` (after cancelling outstanding futures) so the original traceback is
preserved, and drop the now-unneeded first_exc variable. The ThreadPoolExecutor
__exit__ still joins any already-running workers before the exception escapes.
* feat(workflows): lock final fan-out status, drop redundant output write, bound workers
Address third review pass:
- Remove the unlocked `context.steps[step_id]["output"] = …` writes in the
fan-out parent update. context.steps[step_id] is the same dict object that
set_step_output() updates under the run lock, so the direct (unsynchronized)
mutation was redundant.
- Preserve sequential halt semantics under concurrency: a later in-flight item
could overwrite state.status after the halting item was identified. _run_fan_out
now derives the halting item's run status (item_halt_status, replacing the bool
item_halted) and restores it after the pool joins, so the final status is the
first halting item's outcome.
- Bound the pool: workers = min(max_concurrency, len(items)) and early-return for
empty items, so a user-controlled max_concurrency can't over-allocate threads.
Add coverage that an earlier PAUSED item's status wins over a later concurrent
FAILED item.
* feat(workflows): avoid unlocked context.steps writes when it aliases step_results
On a resume run, StepContext is built with steps=state.step_results, so the two
direct `context.steps[...] = ...` writes mutated the shared dict outside the run
lock and could race save(). Route both through a new _record_result helper that
mirrors into context.steps only when it is a distinct object (a fresh run) and
otherwise relies solely on record_step_result's locked write.
* fix(codebuddy): repoint install_url to codebuddy.cn (#3172)
The codebuddy.ai domain no longer resolves; CodeBuddy consolidated onto
codebuddy.cn (Tencent). Update install_url and docs links to
https://www.codebuddy.cn/cli (verified live).
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: use canonical 'CodeBuddy' capitalization in installation prereqs
Address Copilot review: the link text read 'Codebuddy CLI' while the rest of
the docs and the integration metadata use 'CodeBuddy'.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`CatalogStackBase._validate_catalog_url` (inherited by `IntegrationCatalog`)
and `PresetCatalog._validate_catalog_url` checked `parsed.netloc`, which is
truthy for host-less URLs like `https://:8080` (port only) or `https://user@`
(userinfo only). Such URLs slipped past validation despite the error message
promising "a valid URL with a host", then failed later with a confusing fetch
error.
Switch both validators to `parsed.hostname` (None for those inputs), matching
the workflow, step, and bundler catalog validators that already do this.
Add regression tests covering port-only and userinfo-only URLs for both
validators.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: bump version to 0.12.1
* chore: begin 0.12.2.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* chore: align CI Python matrix with devguide release lifecycle
Run the pytest matrix only on the bugfix (maintenance) releases — 3.13
and 3.14 — instead of 3.11/3.12/3.13, and point the ruff lint job at the
latest interpreter (3.14). The supported floor stays at requires-python
>= 3.11 (oldest non-EOL security release): older security versions are
supported by claim and fixed reactively rather than gated on a wide
per-commit matrix. Also add macos-latest to the OS matrix so macOS
regressions are caught.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: make bash scripts portable to bash 3.2 (macOS system /bin/bash)
Adding macos-latest to the CI matrix surfaced two pre-existing bash 3.2
incompatibilities (macOS ships bash 3.2 as /bin/bash):
1. update-agent-context.sh embedded Python heredocs inside $(...) command
substitution. bash 3.2 mis-parses an apostrophe in a heredoc body
nested in $(...), failing with "unexpected EOF while looking for
matching `''". Removed the apostrophes from the affected $()-nested
heredoc body and documented the constraint to prevent regressions.
2. create-new-feature-branch.sh and create-new-feature.sh used the
bash 4+ ${word^^} uppercase parameter expansion, which errors as a
"bad substitution" on bash 3.2 and caused short uppercase acronyms
(e.g. "GO") to be dropped from derived branch names. Replaced with a
portable `tr '[:lower:]' '[:upper:]'` pipeline.
Verified the full test suite passes under bash 3.2.57 and shellcheck
(--severity=error) is clean.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address review feedback on bash 3.2 portability changes
- create-new-feature.sh: replace the non-portable `\b...\b` grep
word-boundary (BSD grep treats `\b` as a backspace, so the acronym
branch could silently fail) with `grep -qw`, matching its twin
create-new-feature-branch.sh, and pipe the description via
`printf '%s'` instead of `echo`.
- create-new-feature-branch.sh: switch the acronym check to
`printf '%s'` as well so both twins are identical and avoid `echo`
on user-provided text.
- update-agent-context.sh: reword the apostrophe-free self-seeding
comment to be clearer and less easy to misread.
Verified under bash 3.2.57 (full bash-script suite green) and
shellcheck --severity=error.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: stop check-prerequisites --paths-only from writing feature.json (#3025)
check-prerequisites --paths-only / -PathsOnly is documented as pure,
read-only path resolution, but when SPECIFY_FEATURE_DIRECTORY was set it
called the persist routine and rewrote .specify/feature.json. That dirtied
the working tree and overwrote a pinned feature directory during what should
be a no-op.
Add an explicit opt-out at the resolver boundary instead of a global env
back-channel:
- bash: get_feature_paths accepts a leading --no-persist flag that skips
_persist_feature_json; check-prerequisites.sh passes it in --paths-only mode.
- PowerShell: Get-FeaturePathsEnv gains a -NoPersist switch that skips
Save-FeatureJson; check-prerequisites.ps1 passes it in -PathsOnly mode.
Normal (non-paths-only) invocations are unchanged and still persist the
override, so future sessions without the env var keep working.
Add regression tests asserting --paths-only/-PathsOnly leaves a pinned
feature.json untouched even when the env override differs, plus a guard that
normal mode still persists.
* fix: use ASCII hyphen in common.ps1 comment for PS 5.1 compatibility
The em-dash in the persist comment introduced non-ASCII bytes, failing
test_ps1_file_is_ascii_only which enforces ASCII-only PowerShell sources
for Windows PowerShell 5.1 compatibility.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: add PowerShell normal-mode persistence guard (#3025)
Addresses Copilot review feedback on #3190: the bash side had a
`test_normal_mode_still_persists_feature_json` guard, but there was no
symmetric PowerShell test asserting that running check-prerequisites.ps1
*without* -PathsOnly still persists the SPECIFY_FEATURE_DIRECTORY override
into .specify/feature.json.
Add test_ps_normal_mode_still_persists_feature_json, which guards against
accidentally passing -NoPersist unconditionally (or flipping the default)
in a future refactor. Verified it fails when -NoPersist is passed in the
non -PathsOnly branch and passes with the current conditional.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: document integration catalog subcommands
the integration reference omits the 'specify integration catalog'
subcommand group (list/add/remove) that exists in code, while the
extension, preset, and workflow references all document their catalog
equivalents. add a catalog management section matching that structure.
* docs: address review feedback on integration catalog section
- catalogs are consulted by the discovery commands (search/info), not
install; install resolves from the built-in registry
- 'catalog list' shows project sources as removable only when configured,
otherwise active sources are non-removable
* fix(scripts): use ASCII [OK] marker in initialize-repo.sh (parity with PowerShell twin)
initialize-repo.sh printed its success line with a Unicode checkmark ('✓ Git repository initialized'), while the PowerShell twin initialize-repo.ps1 and both auto-commit scripts use the ASCII marker '[OK]'. That is an output-text divergence across the bash/PowerShell twins and an inconsistency among sibling extension scripts. Use '[OK]' to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: assert full [OK] init line and surface stderr on failure
Address Copilot review: assert the full success line '[OK] Git repository initialized' (not just the '[OK]' substring, which could pass if unrelated [OK] output is added later) and include result.stderr in the assertion message so a failure is debuggable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: document integration search/info/scaffold subcommands (#3174)
docs/reference/integrations.md omitted three subcommands that exist in
code, breaking parity with the extension/preset/bundle/workflow
references which all document their search/info equivalents.
Added sections for:
- `specify integration search [query]` (--tag, --author)
- `specify integration info <integration_id>`
- `specify integration scaffold <key>` (--type: markdown/skills/toml/yaml)
Content mirrors the command docstrings, arguments, and options in
src/specify_cli/integrations/_query_commands.py and _scaffold_commands.py.
Fixes#3174.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* docs: remove Cursor from specify check agent list (#3178)
Cursor is registered as an IDE-based integration (requires_cli=False),
so `specify check` never probes for a "Cursor CLI". Listing it in the
README's check description misled users into expecting a check that
does not happen. Removed it from the list; the remaining entries all
correspond to integrations with requires_cli=True.
Fixes#3178.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(goose): repoint install_url and docs to goose-docs.ai (#3171)
Goose moved to the Agentic AI Foundation; docs moved from block.github.io/goose
to goose-docs.ai. Update install_url and the docs reference link.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(goose): restore table column alignment
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 'template not found' fallback used Write-Warning, which emits 'WARNING: Plan template not found' on the warning stream -- diverging from the bash twin (echo 'Warning: Plan template not found' to stderr in --json, stdout in text mode) in both wording and routing, and inconsistent with the sibling 'Copied plan template' message (#3198) in the same block. Route it the same way so the two scripts share one status-output contract.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The bundle command group's _fail() helper is documented as printing 'to stderr', and the module contract is 'human logs go to stderr/console' while --json 'emits machine-readable data on stdout'. But it called console.print(), and the shared console writes to STDOUT, so every bundle error (every command routes through _fail) landed on stdout -- corrupting the JSON stream that --json consumers parse.
Add a stderr-bound err_console to _console.py (its documented role as the single Console source) and use it in _fail. stdout now carries only the JSON payload.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: bump version to 0.12.0
* chore: begin 0.12.1.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* docs: add Spec Kit spec for agent-context full opt-in
Use Spec Kit's own specify workflow to author the spec that makes the
agent-context extension a full opt-in, removing all agent-context
configuration/support from the Python codebase and removing the
deprecation message. Force-added despite specs/ being gitignored; the
generated artifact will be purged prior to merge.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: add Spec Kit plan artifacts for agent-context full opt-in
Phase 0/1 of the SDD plan workflow: plan.md, research.md, data-model.md,
quickstart.md, and contracts/cli-behavior.md. Constitution Check is a
documented no-op (repo has no ratified constitution). Force-added despite
specs/ being gitignored; generated artifacts will be purged prior to merge.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: correct Constitution Check against ratified v1.0.0
Earlier draft wrongly treated the gate as a no-op; the fork's main is 16
commits behind upstream/main, which carries .specify/memory/constitution.md.
Re-evaluate the feature against Principles I-V (all PASS) and note that
Principle I mandates keeping context_file as a declared class attribute,
validating the R1 metadata decision.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: refresh plan artifacts against synced upstream/main
After syncing fork main to upstream and rebasing, re-scan the current
agent-context surface. Upstream generalized the single context_file into a
plural context_files concept with new resolver helpers
(_resolve_context_files, _resolve_context_file_values,
_format_context_file_values) and upsert/remove now loop over multiple
files. Update research.md, data-model.md, contracts, quickstart grep
guards, and the plan summary to cover the expanded removal scope.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: add Spec Kit tasks for agent-context full opt-in
Phase 2 of SDD: dependency-ordered tasks.md (30 tasks) organized by the
three user stories, with mandatory test tasks (Constitution Principle II)
and a foundational phase decoupling __CONTEXT_FILE__ resolution from the
extension config. Includes the extension self-seeding task (T015) and a
static guard test (T002) enforcing zero agent-context references in the CLI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat!: remove agent-context lifecycle from the Specify CLI
Make the agent-context extension a full opt-in. The CLI no longer
installs the extension during init, writes agent-context-config.yml,
or creates/updates/removes the managed Spec Kit section in agent
context files. Context-section upsert/remove, marker resolution,
extension-enabled gating, the config helpers, and the obsolete inline
deprecation warning are all removed. Integration context_file stays as
inert metadata; __CONTEXT_FILE__ now resolves from registry metadata.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(agent-context): self-seed context file from the active integration
When agent-context-config.yml has no context_file/context_files, the
bundled bash and PowerShell update scripts now resolve the context file
from the active integration in .specify/init-options.json via the
integration registry, so the extension no longer depends on the CLI
writing its config.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test+docs: update suite and docs for agent-context opt-in
Update integration/extension tests to expect no agent-context install,
config, or context-section writes during init. Add a static guard test
(test_agent_context_cli_free.py) asserting the CLI source is free of
agent-context lifecycle symbols, plus backward-compatibility tests for
legacy projects. Refresh AGENTS.md, the extension README, and add a
CHANGELOG entry describing the opt-in behavior change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(agent-context): warn on self-seed failure, correct docs, speed up guard test
Address PR review feedback:
- Self-seed scripts (bash + PowerShell) now emit an actionable warning when
an active integration is configured but specify_cli cannot be imported by
the chosen Python (e.g. pipx installs), or when the integration declares no
context file, instead of silently falling through to 'nothing to do'.
- Correct the extension README disable note: command rendering never reads the
extension config; __CONTEXT_FILE__ is always substituted from integration
metadata, so a stale context_files value cannot affect rendering.
- Cache CLI source reads in the static guard test via a module-scoped fixture
so the directory walk happens once instead of once per forbidden symbol.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(agent-context): ship self-owned per-agent context-file defaults
The extension now bundles agent-context-defaults.json (key→context_file
map) and self-seeds from it, dropping any dependency on the Specify CLI
registry. Both the bash and PowerShell update scripts read the bundled
JSON map keyed by the active integration from init-options.json.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat!: remove all agent-context state from the Specify CLI
Strip every context_file reference from the CLI: the field on all 35
integration classes, the IntegrationBase plumbing (process_template
param/step, _context_file_display, docstrings), the __CONTEXT_FILE__
resolution in agents.py, the legacy context_file/context_markers
popping in _helpers.py, and the context_file template in
integration_scaffold.py. Also drop the Agent context update step and
__CONTEXT_FILE__ placeholder from templates/commands/plan.md.
The agent-context extension now solely owns all context-file knowledge,
including the per-agent default mapping.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: drop context_file coverage and guard against CLI reintroduction
Remove CONTEXT_FILE attrs and context_file assertions across the base
mixins, all 35 per-integration test files, shared integration tests, and
conftest stubs. Rewrite the base-mixin context tests to assert no managed
section is written and no __CONTEXT_FILE__ placeholder survives. Extend
the CLI-free static guard to forbid context_file, __CONTEXT_FILE__, and
_context_file_display in src/specify_cli, and have the extension tests
copy the bundled defaults JSON so self-seed runs without the CLI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: reflect full removal of agent-context state from the CLI
Update AGENTS.md (integration examples, required-fields table, context
behavior section, pitfalls), CHANGELOG, and the SDD spec artifacts
(FR-007, SC-002, data-model) to state that the CLI carries no
context_file and the extension fully owns the per-agent default mapping
via agent-context-defaults.json.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: align SDD artifacts with full context_file removal
Update research.md (R1, R2, R4, summary table), contracts/cli-behavior.md
(C3, C5), tasks.md (Phase 2, T026, notes), plan.md (Principle I, source
map), and checklists/requirements.md so the spec artifacts reflect the
implemented decision: the CLI carries no context_file attribute or
__CONTEXT_FILE__ resolution, and the per-agent defaults map lives in the
extension. Resolves PR review #4548130110.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: scrub stale context-file mentions from CLI docstrings
Update the multi_install_safe docstring (drop the removed "context file"
invariant), the RovoDev setup docstring (no longer upserts a context
section), the Copilot module docstring (drop the context-file line), and
tighten the _update_init_options_for_integration note. Pure docstring
changes — no behavioral impact. Resolves PR review #4548237085.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test+docs: harden agent-context test helper and fix stale docs
- base.py: document multi_install_safe as an optional subclass attribute
in the IntegrationBase docstring.
- test_cli.py: clarify the init-options assertion is guarding against
leftover legacy agent-context keys, not relocation.
- test_extension_agent_context.py: _install_agent_context_config now
asserts the bundled agent-context-defaults.json exists and always
copies it, so self-seeding tests fail loudly instead of silently
skipping when the map is missing.
- test_integration_cursor_agent.py: drop Path/IntegrationManifest imports
left unused after removing the context-section frontmatter tests.
Resolves PR review #4548293116.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: remove gitignored SDD artifacts from specs/
The specs/001-agent-context-full-optin/ artifacts were force-added for
dogfooding visibility, but specs/ is gitignored and these were always
intended to be purged before merge. Remove them so merging does not add
an intentionally-untracked directory to repo history.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: keep CHANGELOG.md identical to upstream
CHANGELOG.md is auto-generated at release time, so the branch should not
carry a manual entry. Restore it to match upstream/main exactly.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: preserve Cursor .mdc frontmatter in agent-context updater scripts
The bundled agent-context updater scripts wrote the managed section as
plain text. For Cursor-style `.mdc` targets this dropped the required
`---\nalwaysApply: true\n---` frontmatter, reintroducing the rule-loading
bug originally fixed in #1699. Port the `_ensure_mdc_frontmatter` logic
into both the bash and PowerShell updaters: prepend frontmatter when
missing, repair `alwaysApply` when set to the wrong value, and leave
non-`.mdc` targets untouched. Add regression tests covering both shells.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: scope CLI-free guard to agent-context-specific symbols
Drop the bare "context_file" substring from FORBIDDEN_SYMBOLS so the
guard no longer fails on unrelated future CLI fields named context_file.
The list still covers agent-context-specific identifiers (__CONTEXT_FILE__,
_context_file_display, _resolve_context_files, _resolve_context_file_values).
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: harden agent-context bash self-seed against malformed init JSON
Two robustness fixes in the embedded Python self-seed logic:
- Coerce the integration value from init-options.json to a string only when
it is actually a string; otherwise treat it as unset so a corrupted
dict/list value degrades to the existing nothing-to-do behavior instead of
breaking the agents-map lookup.
- Normalize agent-context-defaults.json: only use 'agents' when both the JSON
root and the 'agents' value are dicts, so a wrong-shaped (but valid) JSON
falls back to the warning path instead of raising on .get.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: correct PowerShell hyphenated key lookup and regex replace count
- Self-seed now reads the defaults mapping via
$defaults.agents.PSObject.Properties[$integrationKey].Value instead of
member access ($defaults.agents.$integrationKey), which parsed hyphenated
keys like 'cursor-agent'/'kiro-cli' as subtraction and failed to resolve.
- Replace the static [regex]::Replace(..., 1) call, whose trailing 1 was
interpreted as RegexOptions.IgnoreCase rather than a replacement count, with
an instance Regex whose Replace(input, replacement, 1) limits to the first
match as intended.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: make bash .mdc frontmatter guard case-insensitive
The bash updater only injected Cursor .mdc frontmatter when ctx_path ended
in lowercase '.mdc', so a mixed/upper-case extension (e.g. specify-rules.MDC)
was skipped and Cursor would not auto-load the rule file. Compare against the
casefolded path. The PowerShell variant already uses -match, which is
case-insensitive by default, so no change is needed there.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: document separator-agnostic agent-context update invocation
The README hard-coded the dot-notation slash command
(/speckit.agent-context.update), which hyphen-separator agents like Forge and
Cline do not recognize. Document the canonical command ID plus both slash
invocations so users copy the form their agent accepts.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The Step Types table in docs/reference/workflows.md listed command, prompt, shell, gate, if, switch, while, do-while, fan-out, and fan-in, but omitted 'init' -- which IS a registered built-in (workflows/__init__.py _register_builtin_steps registers InitStep) and is documented in steps/init/__init__.py as bootstrapping a project (equivalent to 'specify init'). Add the missing row so the reference matches the registry.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GateStep.validate() reports non-string options as an error, but then -- when on_reject is 'abort'/'retry' -- still runs the reject-choice check 'any(o.lower() in ... for o in options)'. For a non-string option (e.g. options: [123]) o.lower() raised AttributeError, which escaped validate() and broke validate_workflow's documented 'return a list of errors, never raise' contract. Guard the check so it only runs when every option is a string (the non-string case is already reported above).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_evaluate_simple_expression used 'if "|" in expr' / expr.split("|", 1) to detect a filter pipe, so a literal '|' inside a quoted operand (e.g. inputs.x == 'a|b') was mistaken for a filter separator and raised a spurious ValueError ('unknown filter') instead of comparing the string. Use the existing quote/bracket-aware _find_top_level helper (added for the operator-splitting fix) so only a top-level pipe is treated as a filter separator.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): reject a fan-in wait_for that names an unknown step at validation
* fix(workflows): reject fan-in wait_for self-reference and non-string entries
Address review feedback on the fan-in wait_for validator:
- A fan-in's own id is added to seen_ids before the wait_for check, so
`wait_for: [<self>]` passed validation while producing a silent empty
join at runtime. Reject self-references explicitly.
- Non-string entries (e.g. YAML `wait_for: [123]`) were skipped by the
isinstance(str) guard and validated even though they can never match a
real step id. Flag them as wiring errors.
Add coverage for both cases.
* fix(scripts): warn when spec template is missing in create-new-feature.ps1 (parity with bash)
create-new-feature.sh prints 'Warning: Spec template not found; created empty spec file' to stderr when no spec template resolves, then touches an empty spec. The PowerShell twin created the empty file silently with no warning, so on Windows a missing/broken template tree gave no signal. Emit the same warning on stderr (keeps stdout/JSON pure), matching the bash wording and stream.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: assert create-new-feature.ps1 warns on missing spec template
Regression test for the bash/PowerShell parity fix: with no resolvable spec template, the PowerShell script must emit 'Spec template not found' on stderr (matching bash) while keeping stdout parseable JSON and still creating the empty spec file. Gated on pwsh; decodes stdout/stderr as UTF-8.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(scripts): count subdirectory-only dirs as non-empty in PowerShell
Test-DirHasFiles (the documented PowerShell twin of bash check_dir) tested
non-emptiness with `Get-ChildItem | Where-Object { -not $_.PSIsContainer }`,
counting only top-level FILES and ignoring subdirectories. Bash check_dir
(`-n $(ls -A ...)`) and the PowerShell JSON-path contracts checks
(check-prerequisites.ps1 / setup-tasks.ps1, no PSIsContainer filter) both
count ANY entry. So a contracts/ directory whose only contents are
subdirectories (e.g. contracts/v1/openapi.yaml) was reported present by
bash, by bash JSON, and by PowerShell JSON, but [FAIL]/absent by PowerShell
text mode — the lone outlier.
Drop the PSIsContainer filter so Test-DirHasFiles counts any entry, matching
the other three code paths.
Add bash + PowerShell parity tests asserting a subdir-only contracts/ dir is
reported non-empty in both shells.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* review: accurate non-empty comment + drop doubled test prefix
Address review feedback on Test-DirHasFiles parity fix:
- Reword the common.ps1 comment so it no longer claims exact `ls -A` parity (Get-ChildItem omits hidden entries without -Force); it now points at the in-repo PowerShell JSON contracts checks as the matching reference and keeps the subdir-only-is-non-empty rationale.
- Rename test_test_dir_has_files_ps_... -> test_dir_has_files_ps_... to drop the doubled 'test_' prefix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: assert dir-non-emptiness via stdout marker, not exit code
Address Copilot review: check_dir always exits 0 (it echoes the marker rather than setting an exit code) and Test-DirHasFiles returns a boolean (pwsh still exits 0 when it returns $false), so 'result.returncode == 0' validated nothing. Drop the misleading assertion and rely on the [OK]/checkmark marker in stdout, which is the actual behavioral signal; document why inline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: keep common.ps1 ASCII-only (PowerShell 5.1 compatibility)
My reworded Test-DirHasFiles comment introduced an em dash (U+2014), which tripped tests/test_ps1_encoding.py::test_ps1_file_is_ascii_only -- .ps1 files must stay ASCII for Windows PowerShell 5.1. Replace it with '--', matching the existing comment style in this file (e.g. the Resolve-SpecifyInitDir docstring).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: decode dir-parity subprocess output as UTF-8 explicitly
Address Copilot review: check_dir echoes the non-ASCII markers ✓/✗, and subprocess.run with text=True but no encoding decodes via the platform locale (cp1252 on Windows), which can raise UnicodeDecodeError or mangle stdout. Pin encoding='utf-8' on both the bash and PowerShell dir-parity helpers so decoding is deterministic across CI runners.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(scripts): drop HAS_GIT from PowerShell git-extension output (parity with bash)
create-new-feature-branch.ps1 emitted a HAS_GIT key in its JSON output and a 'HAS_GIT:' line in text output that the bash twin never emits. The bash output contract is {BRANCH_NAME, FEATURE_NUM} (+ DRY_RUN) only, so a tool parsing the machine-readable output got a different shape on Windows/PowerShell vs macOS/Linux -- a cross-platform contract divergence.
$hasGit is still computed and used internally for branch-creation logic; only its two output emissions are removed, restoring parity. Added regression tests asserting neither the PS nor the bash output contains HAS_GIT (JSON and text). Noted as a follow-up in #3129.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: note DRY_RUN in the HAS_GIT-omission comment (parity)
Address Copilot review: the comment described the output contract as {BRANCH_NAME, FEATURE_NUM} without mentioning that DRY_RUN is still conditionally added in JSON mode on dry runs. Clarify so the contract description is complete for future maintainers. Comment-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: bump version to 0.11.10
* chore: begin 0.11.11.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(extensions): apply GHES auth and resolve release assets for --from
The 'specify extension add --from <url>' path fetched ZIPs via a bare
open_url with no GitHub release-asset resolution and no Accept header,
diverging from the catalog download path. Against GHES it received an
HTML login page and failed obscurely with zipfile.BadZipFile.
Route --from through ExtensionCatalog so configured GHES credentials
apply and release-download URLs resolve via /api/v3, and reject non-ZIP
content with a clear error pointing at auth.json.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(extensions): use zipfile.is_zipfile for --from content guard
Replace the weak zip_data.startswith(b"PK") prefix check with
zipfile.is_zipfile() on a BytesIO so any non-ZIP payload (not just
those lacking the PK magic) is rejected with the friendly error before
install_from_zip can raise BadZipFile. Addresses PR review feedback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The @mariozechner/pi-coding-agent npm package is deprecated in favor of
@earendil-works/pi-coding-agent. Pi Coding Agent is still active under the
new org, so update the install_url rather than removing the integration.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
the shared CatalogStackBase validator and PresetCatalog validator
checked parsed.netloc to enforce 'a valid URL with a host'. but netloc
is truthy for host-less URLs like https://:8080 or https://user@, so
those slipped through even though they have no host - contradicting the
error message. the workflow, step, and bundler validators already check
parsed.hostname (which is None in those cases); this aligns the two
stragglers with that. add regression tests covering port-only and
userinfo-only URLs.
WorkflowEngine._coerce_input normalizes a whole-valued number to int via int(value). For an infinite float (e.g. a 'type: number' input with YAML 'default: .inf') int(inf) raises OverflowError, which is not in the except (ValueError, TypeError) tuple. validate_workflow eager-coerces declared defaults and is documented to RETURN a list of errors, but it only catches ValueError -- so the OverflowError escaped and validate_workflow raised instead of reporting, breaking its contract. (NaN already surfaced cleanly because int(nan) raises ValueError.)
Add OverflowError to the except tuple so an infinite default surfaces as the same clean 'expected a number' ValueError as NaN, consistent with the function's existing fail-fast-on-authoring-mistakes design. Finite values (5.0 -> 5, 3.5 -> 3.5) are unaffected.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
setup-plan.sh prints 'Copied plan template to $IMPL_PLAN' after copying the template (to stderr in --json mode, stdout otherwise), but the PowerShell twin emitted nothing on the successful-copy path -- only the 'Plan already exists' skip message and the 'Plan template not found' warning existed. So the two scripts had a divergent status-output contract on a fresh run.
Emit the same message after WriteAllText, routed like the sibling skip message ([Console]::Error.WriteLine in -Json so stdout stays pure JSON, Write-Output in text mode). Mirrors the bash wording and stream routing exactly.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_evaluate_simple_expression split on operator keywords using naive str.find/split, so a keyword INSIDE a quoted operand was treated as an operator: `inputs.mode == 'read and write'` split on the inner ' and ' and evaluated as `(inputs.mode == 'read) and (write')`. The literal short-circuit was also too greedy -- `'a' == 'b'` matched startswith("'")/endswith("'") and was stripped to the garbage truthy string `a' == 'b`, so `'done' == 'failed'` evaluated truthy and gated the wrong branch.
Add a quote/bracket-aware _find_top_level helper (mirroring the existing _split_top_level_commas) and use it for the and/or/comparison/in/not-in splits; tighten the literal short-circuit to fire only when the opening quote's match is the final char. The docstring already lists comparisons + and/or/not + in/not-in + string literals as supported, so this restores the documented contract.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Get-BranchName used `[long]$Number = 0` as both the default and the 'auto-detect' sentinel (`if ($Number -eq 0)`), so an explicitly-passed `-Number 0` was indistinguishable from 'not supplied' and silently auto-incremented. The bash twin keys off whether the value is non-empty (`[ -z "$BRANCH_NUMBER" ]`), so `--number 0` is honored and yields FEATURE_NUM 000 -- a cross-platform divergence for identical input.
Use $PSBoundParameters.ContainsKey('Number') instead, so an explicit value (including 0) is honored and only a missing -Number auto-detects -- mirroring bash. This also aligns the -Timestamp+-Number warning, which bash emits for `--number 0 --timestamp` (non-empty check) but PowerShell previously skipped.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: bump version to 0.11.9
* chore: begin 0.11.10.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
PR #2511 added `context: fork` + `agent: general-purpose` to the generated
speckit-analyze SKILL.md on the assumption that its heavy reads collapse to a
short summary. In practice /speckit-analyze returns a 300-500 line report that
is injected back into the main conversation. In long sessions each subsequent
fork inherits that growing context, compounding overhead until the chat
freezes (#3185).
Empty FORK_CONTEXT_COMMANDS so no command opts into context: fork, restoring
direct in-session execution for analyze. The injection mechanism is retained
so a command can be re-enabled once it genuinely returns a compact result.
Fixes#3185
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: derive plan path from feature.json in update-agent-context
When `plan_path` is omitted, prefer `.specify/feature.json`
(written by /speckit-specify) over the mtime heuristic. The
old approach picked the most recently modified `specs/*/plan.md`,
which could inject an unrelated plan into CLAUDE.md if another
spec's plan was touched after the active feature directory was
created but before its own plan.md existed.
Bash: handle both relative and absolute feature_directory values,
normalizing absolute paths back to project-relative for the
context file. Fall back to mtime only when feature.json is absent
or the derived plan.md does not yet exist.
PowerShell: same logic, PS 5.1-compatible (nested Join-Path,
IsPathRooted guard to avoid Unix Join-Path mis-joining absolute
ChildPaths, manual prefix-strip instead of GetRelativePath).
Fixes#3067
* fix: address Copilot review feedback on update-agent-context
- bash: add explicit encoding="utf-8" to feature.json open() call
- powershell: replace GetRelativePath (.NET 5+ only) with manual
prefix-strip in mtime fallback for PS 5.1 compatibility
- tests: add coverage for absolute feature_directory values
(under and outside PROJECT_ROOT)
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* test: replace time.sleep with os.utime and strengthen PS normalization assertion
* Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix: normalize trailing slash and guard non-string feature_directory in PS script
* Fix: use .resolve().as_posix().
Valid. The PS tests run on Windows where str(tmp_path) uses backslashes, but the PS script normalizes output to forward slashes. Assertions like assert str(tmp_path) not in ctx become false negatives on Windows CI.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix: use context manager for feature.json open() in bash heredoc
* test: add PS coverage for absolute feature_directory outside project root
* fix: guard null feature_directory, re-check empty after trailing-slash strip, fix blank line
* test: add stale plan to absolute-path tests so feature.json preference is actually exercised
* test: convert absolute paths to MSYS2 style for Git-for-Windows bash compatibility
* fix: revert PS test to native path, fix bash outside-root assertion for Git bash
* fix: use _to_bash_path in not-in assertion for Git bash Windows compat
* fix: add ConvertFrom-Json fallback in PS script, write test config as JSON
* fix: use OS-appropriate StringComparison in PS prefix-strip (matches common.ps1)
* fix: emit project-relative POSIX path from mtime fallback; use upstream test helpers
* fix: write config as JSON directly, drop _install_agent_context_config
* fix: normalize backslashes to forward slashes in feature_directory before path ops
* fix: treat drive-qualified paths (C:/...) as absolute after backslash normalization
* fix: resolve symlinks when computing relative plan path; use UTF8 encoding in PS ConvertFrom-Yaml path
* fix: use bash-side path for outside-root case to avoid WindowsPath backslashes
* fix: use .as_posix() instead of PurePosixPath() to avoid backslashes on native Windows Python
* fix: resolve ./.. segments in PS feature_directory via GetFullPath before relativizing
* fix: replace $IsWindows guard with OSVersion.Platform check for PS 5.1 StrictMode compat
* fix: guard empty relDir to avoid leading slash in PlanPath when feature_directory is project root
* fix: remove unused PurePosixPath import; fix stale PS comment after ConvertFrom-Json fallback was added
* fix: use cand.as_posix() for outside-root path instead of raw bash-side argv
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(catalog): point companion documentation at README.md so it renders
The companion entry's documentation URL pointed at a directory
(speckit-extension/docs/), which the community site can't fetch as
markdown — its extension page renders an empty README (readmeContent:
null). Every other catalog entry points documentation at a specific
README.md (or .md file). Point companion at its extension README so the
page renders.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(catalog): companion → stable companion-latest download_url, v0.4.1, sharper tags
- download_url now points at the rolling companion-latest asset so by-name install always serves the newest build (no per-release catalog PR)
- version 0.3.0 → 0.4.1
- tags: drop redundant 'companion'/'progress'/'lifecycle', add spec-driven-development, spec-kit, turbo, capture
* fix(catalog): companion tags → capability-first (vscode, progress, status, resume, configurable, extensible)
Tags now name what Companion adds over stock spec-kit, in browse-able terms — dropped catalog-noise (spec-kit, spec-driven-development) and insider jargon (turbo, capture).
* fix(catalog): pin companion to speckit-ext-v0.8.0 asset; sync entry
Pin download_url to the version-matched release asset (every other catalog
entry pins to a tag; the floating companion-latest URL made installs
non-reproducible). Bring the entry up to v0.8.0: version 0.4.1 -> 0.8.0,
commands 10 -> 12, speckit_version floor >=0.9.5.dev0, and drop the removed
"turbo pipeline profile" from the description in favor of the hooks/recipes
customization that shipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(catalog): bump companion to v0.11.0 (latest released asset, 13 commands)
* fix(catalog): companion speckit_version floor >=0.9.5 (drop pointless .dev0)
* fix(catalog): align companion updated_at with catalog root (2026-06-24)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(auth): add github_provider_hosts() to enumerate GHES hosts from auth.json
Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous)
* fix(extensions): resolve GHES release assets via /api/v3
Generalizes resolve_github_release_asset_api_url to GitHub Enterprise
Server hosts (gated by auth.json github hosts), fixing private GHES
extension/preset downloads. github/spec-kit#3147
Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous)
* fix(extensions,presets): pass auth.json github hosts into release resolver
Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous)
* docs(auth): document GHES private catalog + release-asset auth
Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous)
* fix(presets,workflows): pass auth.json github hosts into remaining release resolvers
Wires preset add --from and workflow add through github_provider_hosts()
so private GHES release assets resolve via /api/v3 there too. github/spec-kit#3147
Assisted-by: Claude Code (model: claude-sonnet-4-6, autonomous)
* test(presets): use module-level io.BytesIO in GHES preset test
Addresses Copilot review on PR #3157: drop unnecessary __import__("io")
in test_preset_add_from_ghes_release_url_resolves_via_api_v3 since io is
already imported at module level.
* fix(github-http): pass through GHES asset API URLs by path shape
Addresses Copilot review on PR #3157. A direct GHES /api/v3 release asset
URL was only returned as already-resolved when its host was in the
allowlist; otherwise the resolver returned None and the caller downloaded
the same URL without 'Accept: application/octet-stream', fetching JSON
metadata instead of the binary.
Gate the passthrough on path shape alone, mirroring the github.com case.
This is safe: passthrough returns the input URL unchanged and the caller
fetches it either way, so no new request to an arbitrary host is induced;
the token stays independently gated by auth.json in open_url. The
allowlist remains the anti-SSRF gate on the tag-lookup resolving path.
Add test_passthrough_for_unlisted_ghes_api_asset_url.
* fix(scripts): keep PowerShell branch-name acronym match case-sensitive
Get-BranchName keeps a sub-3-character word only when it appears as an
UPPERCASE acronym in the description. The bash twin checks this
case-sensitively (grep "\b${word^^}\b" / grep -qw -- "${word^^}"), but the
PowerShell twin used -match, which is case-INSENSITIVE, so it kept EVERY
short word regardless of case -- contradicting its own comment and diverging
from bash. The same description then produced different spec-directory and
branch names on Windows/PowerShell vs macOS/Linux (e.g. "Add go support" ->
001-go-support instead of 001-support), desyncing specs/, feature.json, and
git branches across a mixed-OS team.
Use the case-sensitive -cmatch so a short word is kept only for a genuine
uppercase acronym, matching bash. Applied to both the core
scripts/powershell/create-new-feature.ps1 and the git extension's
create-new-feature-branch.ps1.
Add bash + PowerShell regression tests (core and git-extension) asserting a
lowercase short word is dropped while an uppercase acronym is kept.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: fix article grammar in branch-name docstrings
Address review: 'an UPPERCASE acronym' -> 'an acronym in UPPERCASE' across the four branch-name case-sensitivity test docstrings (the indefinite article reads cleanly before 'acronym'). Docstring-only; no behavior change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In agent-direct invocations nothing watches agent output for the
EXECUTE_COMMAND: directive, so a mandatory hook that is only emitted
never runs and the failure is silent (#2730). Add one line after each
mandatory-hook block instructing the agent to actually invoke the hook
and wait for it before continuing.
The instruction tells the agent to run the hook the way it would run
the command itself in the current agent/session, and notes the
invocation may differ from the literal {command} id shown in the block
(e.g. skills-mode agents run it as /skill:speckit-... or $speckit-...),
so it stays correct outside the default slash-command form.
Fixes#2730
* chore: bump version to 0.11.8
* chore: begin 0.11.9.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* docs: add SpecKit Assistant npm package to Community Friends
Adds SpecKit Assistant (https://www.npmjs.com/package/speckit-assistant)
to the Community Friends list. It is a visual interface for the specify
CLI that orchestrates Spec-Driven Development (SDD) — connecting local
specification, planning, and task checklists with AI agents (Claude,
Gemini, Copilot). No installation required; run it via npx speckit-assistant.
As the author of both the VS Code Spec Kit Assistant extension and the
SpecKit Assistant npm package, I maintain these community tools that
provide a visual interface on top of the specify CLI.
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* docs: clarify SpecKit Assistant requires no global installation
Address Copilot review: 'No installation required' was misleading for an
npx-run package since npx still downloads it. Clarify that no global
installation is required.
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Require preset-usage README with Spec Kit CLI syntax in submissions
Tighten the community preset submission workflow so it validates the
README referenced by the documentation field rather than merely checking
for a root README. The workflow now fails submissions whose linked README
lacks a valid 'specify preset add ...' command and flags monorepo
submissions that point documentation at a generic root README.
- Add a required Documentation URL field to the preset issue template
- Add validation step 2d (documentation README + CLI-syntax check) to
.github/workflows/add-community-preset.md and recompile the lock file
- Document the stricter usage-README requirement and reviewer content
check in presets/PUBLISHING.md
Closes#3103
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Align preset README docs with workflow's actual enforcement
Address PR review feedback on #3104:
- PUBLISHING.md: clarify that only README resolution + a valid
'specify preset add ...' command are mechanically enforced; the
preset-scoped-README and minimum-structure items are reviewer
expectations, not automated checks.
- PUBLISHING.md: state that a missing 'specify preset add ...' command
is a hard validation failure (check 2d), not just 'flagged for changes'.
- preset_submission.yml: require 'specify preset add ...' (not the looser
'specify preset ...') to match the workflow validation.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Tighten preset README validation and docs per PR review
Address PR review feedback on #3104:
- Workflow Step 2c: drop the generic repo-root README.md check so the
README requirement is enforced exactly once, in Step 2d, against the
file the documentation field points to (avoids monorepo false-positive).
- Workflow Step 2d: restrict the documentation URL to GitHub-hosted
README URLs (github.com/.../blob/... or raw.githubusercontent.com/...)
before fetching user-provided input.
- PUBLISHING.md: add the required 'id' field to the example catalog entry.
- preset_submission.yml: fix the Documentation URL placeholder to match
the recommended monorepo presets/<id>/README.md pattern.
- Recompile add-community-preset.lock.yml (body hash only).
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Refine preset README validation rules per PR review
Address PR review feedback on #3104:
- Workflow Step 2d: broaden the documentation URL allowlist to also
accept github.com/.../raw/... URLs; strip any fragment/query before
fetching so the target is deterministic; clarify that a
'specify preset add --from <url>' command only counts when its URL
matches the submitted Download URL (a different --from URL does not
satisfy the requirement, though other accepted forms still can).
- PUBLISHING.md: show both accepted download URL shapes (tag archive and
release asset) in the README install example instead of implying only
the releases/download form.
- preset_submission.yml: remove the ambiguous generic 'README.md with
description and usage instructions' checkbox; the linked-README
requirement is the single source of truth.
- Recompile add-community-preset.lock.yml (body hash only).
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Clarify install-command requirement wording per PR review
Address PR review feedback on #3104: the previous 'matching the download
URL' wording overstated the requirement. Only the 'specify preset add
--from <url>' form needs an exact download-URL match; other accepted
forms ('specify preset add <id>' / '--dev <path>') don't reference the
download URL at all.
- preset_submission.yml: reword the Documentation URL description and the
Submission Requirements checkbox to reflect what's enforced vs preferred.
- PUBLISHING.md: clarify the reviewer note so the exact-match rule is
scoped to the --from form.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Require README.md target and fix release-ZIP wording per PR review
Address PR review feedback on #3104:
- Workflow Step 2d: add an explicit check that the documentation URL path
ends with README.md (case-insensitive) after stripping fragment/query,
so a non-README markdown file is rejected before fetching.
- PUBLISHING.md: reword the release-ZIP note, which conflicted with the
earlier preset structure guidance. The real requirement is that the
README is reachable at the documentation URL before download; it's fine
for the same file to also ship inside the release ZIP.
- Recompile add-community-preset.lock.yml (body hash only).
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Use stable unnumbered anchor for Usage README Requirements
Address PR review feedback on #3104: drop the '6.' prefix from the
'Usage README Requirements' heading so its GitHub anchor isn't tied to a
section number (brittle under renumbering, and avoids confusion with the
top-level 'Best Practices' TOC item). Update the Prerequisites cross-link
to the new #usage-readme-requirements anchor.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Align README requirement wording with enforced checks per PR review
Address PR review feedback on #3104:
- PUBLISHING.md: the 'mechanically enforces' summary now lists all Step 2d
checks (GitHub-hosted URL, path ends with README.md, resolves, contains
a valid 'specify preset add ...' command), instead of only two.
- PUBLISHING.md: reword the PR checklist item so a usage README + install
command is the requirement, with preset-scoped README recommended for
monorepos (matches the workflow's flag-not-fail behavior).
- preset_submission.yml: include the full 'specify preset add' prefix on
the --dev and --from forms in the field description and checklist so
submitters copy the exact syntax.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix grammar in Usage README Requirements intro
Address PR review feedback on #3104: remove the incorrect colon after
'the linked README' so the sentence reads naturally.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Avoid lossy raw URL rewrite for slash-containing refs per PR review
Address PR review feedback on #3104: rewriting documentation URLs into the
raw.githubusercontent.com/<owner>/<repo>/<ref>/<path> form can't reliably
represent refs that contain slashes (e.g. a feature/foo branch). Step 2d
now fetches github.com blob URLs by swapping only /blob/ -> /raw/, and
fetches github.com/.../raw/... and raw.githubusercontent.com/... URLs
as-is, instead of reconstructing the raw host form.
Recompile add-community-preset.lock.yml (body hash only).
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(integration): update Kimi integration for Kimi Code CLI
Update the Kimi integration to target the new Kimi Code CLI
(MoonshotAI/kimi-code) layout:
- Change skills directory from .kimi/skills/ to .kimi-code/skills/
- Change context file from KIMI.md to AGENTS.md
- Extend --migrate-legacy to move old .kimi/skills/ installs and
migrate KIMI.md user content to AGENTS.md
- Clean up leftover legacy .kimi/skills/ directories on teardown
- Update devcontainer installer to @moonshot-ai/kimi-code
- Update docs and tests
Relates to #1532
* fix(integration): align Kimi dispatch and harden legacy migration
- Override build_command_invocation to emit /skill:speckit-<stem>
so dispatched commands match Kimi Code CLI's native slash syntax.
- Skip symlinked .kimi/skills directories during legacy migration
and teardown to avoid operating on files outside the project.
- Remove kimi from the multi-install-safe integrations table.
- Add tests for command invocation and symlink safety.
* fix(integration): resolve custom context markers in Kimi legacy migration
Use IntegrationBase._resolve_context_markers() when migrating legacy
KIMI.md content so that projects with customized context_markers in
.specify/extensions/agent-context/agent-context-config.yml have the
managed section stripped with the correct markers instead of the
hard-coded defaults.
Adds a test verifying custom markers are respected during
--migrate-legacy.
* fix(integration): harden Kimi legacy migration against symlinked paths
* fix(kimi): guard symlinked SKILL.md during migration and teardown
* docs(kimi): mention KIMI.md→AGENTS.md migration in --migrate-legacy help
The --migrate-legacy help text listed only the skills directory move and
dotted→hyphenated renaming, but the flag also migrates KIMI.md user content
into AGENTS.md. Align the help with the actual behavior, docs, and tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(kimi): validate legacy migration destination; clarify docstrings
Address Copilot review feedback on PR #2979:
- setup(): gate skills migration on _is_safe_legacy_dir(new_skills_dir)
as well as the source. base setup() already rejects a destination that
escapes the project root, but an in-tree symlinked .kimi-code/skills
(e.g. -> .) could still misdirect the move; this gives the destination
the same symlink-component protection as the source.
- _migrate_legacy_kimi_dotted_skills: rewrite docstring as a compatibility
shim describing same-path delegation to _migrate_legacy_kimi_skills_dir.
- test_presets: clarify that the dotted-skill test exercises legacy naming
under the current .kimi-code/ base, not the legacy .kimi/ location.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(kimi): harden legacy KIMI.md→AGENTS.md context migration
- Skip context-file migration when the agent-context extension is
disabled, matching upsert/remove_context_section opt-out behavior so
an opted-out project's KIMI.md/AGENTS.md are left untouched.
- Safely skip (instead of raising) on filesystem edge cases: unreadable
or non-UTF-8 KIMI.md, and AGENTS.md existing as a non-file/unwritable.
- Refuse to migrate a corrupted managed section (single marker, or end
before start) so a partial managed block is never copied into
AGENTS.md; KIMI.md is preserved for manual repair.
Add regression tests for all three cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Approve fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* chore(kimi): revert CHANGELOG.md edit (auto-generated)
The CHANGELOG is generated from merged PR titles, so a hand-written entry
is redundant; it was also placed under the already-released 0.10.2 section,
which would make those release notes historically inaccurate. Revert to
match main per maintainer feedback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(kimi): skip symlink-safety tests when symlinks are unavailable
The Kimi legacy-migration safety tests create symlinks to assert that
migration/teardown never follow them out of the project. Symlink creation
fails on Windows without the create-symlink privilege and in some restricted
CI sandboxes, so these tests errored during setup instead of skipping.
Wrap every symlink_to() call in a shared _symlink_or_skip() helper that
pytest.skip()s on OSError/NotImplementedError, matching the guard pattern
already used by one of these tests. Verified on Windows: the 6 symlink tests
now skip cleanly (51 passed, 6 skipped) instead of erroring.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(kimi): reject symlinked skills destination before install
Add a destination symlink pre-check in KimiIntegration.setup() before
super().setup() writes any SKILL.md. The base class only rejects a
destination that escapes project_root after resolve(), so an in-tree
symlinked .kimi-code/.kimi-code/skills (e.g. `-> .`) would still
misdirect writes into an unintended in-tree location (./skills/).
Extract the symlink-component walk into a shared _has_symlinked_component()
helper and reuse it from _is_safe_legacy_dir(). Add a regression test.
Also clarify that --migrate-legacy only migrates KIMI.md -> AGENTS.md when
the agent-context extension is enabled, in the CLI help text and the
integration docs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Refactor formatting and simplify logic in Kimi integration
* fix(kimi): reject symlinked target dir during legacy skills migration
When the migration destination already exists, guard against a symlinked
(or non-directory) target_dir before comparing SKILL.md bytes, so the
comparison never follows a link outside the project root. Also skip a
missing/non-file target SKILL.md explicitly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* docs: run /speckit.checklist after /speckit.plan in quickstart
The quickstart workflow showed /speckit.checklist before /speckit.plan,
contradicting the CLI next-steps text (commands/init.py), which lists the
checklist as running after the plan. Per the maintainer on #2816 — "the
docs were actually wrong here ... checklists are meant for after plan" —
align the docs to the CLI: move /speckit.checklist after /speckit.plan in
the workflow diagram, the prose, and both walkthrough step sequences.
Docs-only; no behavior change.
Closes#2606
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: reword checklist as generating quality checklists, not validating directly
Address review: /speckit.checklist generates quality checklists (which then validate the requirements) rather than validating directly, matching the CLI/README phrasing. Preserves the after-plan ordering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: align checklist wording with CLI next-steps phrasing
Address review: state the checklist's purpose (validate requirements completeness, clarity, and consistency) and anchor it to /speckit.plan as the CLI does, use the plural 'quality checklists', and reword the Taskify step so the spec is validated using the generated checklists.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): preserve commas inside quoted list-literal elements
The simple-expression evaluator parsed a list literal with a naive
`inner.split(",")`, which splits on commas inside quoted strings (and
nested brackets). So `{{ ["a, b", "c"] }}` evaluated to three items
(`["a", "b", "c"]`) instead of two, silently corrupting `fan-out` `items:`
and any list expression that contains a comma inside a quoted element.
Split list-literal elements on top-level commas only, ignoring commas
inside quotes or nested brackets, via a small `_split_top_level_commas`
helper. Plain and empty lists are unchanged.
Add tests covering quoted commas, nested lists, and the existing
plain/empty cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(workflows): cover single-quoted and nested list literals
Address review: extend the list-literal regression test to assert single-quoted elements with commas and nested lists parse correctly, alongside the existing double-quoted cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: pin actions to commit SHAs and add shellcheck
Pin actions/github-script in catalog-assign.yml to a full commit SHA; all
other workflows were already pinned. Add a repo-wide regression test that
every workflow `uses:` ref is pinned to a 40-char commit SHA.
Add a shellcheck job to lint.yml (--severity=error over scripts/bash/*.sh)
and document the local command in CONTRIBUTING.md.
* ci: use repo-standard actions/checkout v7.0.0 in shellcheck job
* ci: shellcheck all tracked shell scripts
Assisted-by: Codex (model: GPT-5, autonomous)
* ci: address workflow hygiene review feedback
Assisted-by: Codex (model: GPT-5, autonomous)
* chore: bump version to 0.11.7
* chore: begin 0.11.8.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat(extensions): verify catalog archive sha256 before install
Extension and preset archives were downloaded over HTTPS and unpacked
(with Zip-Slip protection) but their bytes were never checked against a
known digest. Trust rested entirely on TLS and the integrity of the
release host, so a tampered or swapped archive from a compromised
third-party release would be installed silently. Maintainers do not audit
extension code, so consumer-side integrity is the only available defence.
Catalog entries may now pin an optional `sha256` digest. When present, the
downloaded archive is verified before it is written to disk and installed;
a mismatch aborts with a clear error. Entries without `sha256` keep
working unchanged (a DEBUG line records that the download was unverified),
so the change is backwards compatible. The check runs on both download
paths (extensions and presets) via a single shared helper so the two stay
in parity.
- Add `verify_archive_sha256` helper in shared_infra (digest match,
`sha256:` prefix, case-insensitive; DEBUG log when no digest declared)
- Enforce it in ExtensionCatalog.download_extension and
PresetCatalog.download_pack, before the archive is written to disk
- Document the optional `sha256` field in the publishing guides
- Tests: helper unit tests + matching/mismatch/no-digest on both paths
Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
Assisted-by: AI
* fix(extensions): harden sha256 parsing and tidy download test mocks
Follow-up to the review on #3080:
- shared_infra.verify_archive_sha256: strip only a literal `sha256:`
algorithm prefix (case-insensitive) instead of `split(':', 1)[-1]`,
which silently dropped any prefix — so `md5:<64-hex>` was accepted as
if it were a valid SHA-256. Validate that the declared value is exactly
64 hex characters and raise a clear error otherwise, and compare with
`hmac.compare_digest` for a constant-time check. Add tests covering a
malformed digest and a non-`sha256:` prefix (both previously accepted).
- Download test helpers: configure the context-manager mock via
`__enter__.return_value`/`__exit__.return_value` rather than assigning a
`lambda s: s`, which is clearer and independent of the invocation arity.
Assisted-by: AI
Signed-off-by: Zied Jlassi (Architect AI) <6190550+zied-jlassi@users.noreply.github.com>
* fix(extensions): reject a declared-but-empty sha256 instead of skipping verification
verify_archive_sha256 skipped on any falsy expected value, so a present-but-empty digest (e.g. sha256: "" reached via ...get("sha256")) silently disabled the integrity check instead of surfacing the authoring error. Guard on expected is None so only an absent digest skips; blank/whitespace/bare-prefix values fall through to the 64-hex validation and are rejected. Adds a regression test.
Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
* docs(shared_infra): clarify _SHA256_HEX_RE accepts and normalizes uppercase
The comment described the regex as matching '64 lowercase' hex characters,
but verify_archive_sha256 lowercases the declared value (raw.lower()) before
matching, so an uppercase digest is accepted and normalized rather than
rejected. Clarify the comment to avoid misleading future readers.
Addresses Copilot review feedback on shared_infra.py.
Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
* test(presets): cover the no-sha256 backwards-compatible path
Address Copilot review: download_pack's optional sha256 verification was
tested for match/mismatch but not the backwards-compatible path where a
catalog entry has no sha256 (pack_info.get("sha256") is None). Add a
no-sha256 test mirroring the extensions coverage so the helper never
silently becomes mandatory for presets.
Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
---------
Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
Signed-off-by: Zied Jlassi (Architect AI) <6190550+zied-jlassi@users.noreply.github.com>
* fix(workflows): validate requires keys and reject phantom permissions gate
A workflow's `requires` block was parsed but its keys were never
validated, so a typo or an unsupported key was silently ignored. Most
importantly, authors could write `requires.permissions.shell: true`
expecting a runtime capability gate — but no such gate exists: a `shell`
step always runs with the user's privileges. The declaration gave a
false sense of sandboxing.
`validate_workflow` now accepts only the recognised keys
(`speckit_version`, `integrations`, `tools`, `mcp`) and rejects anything
else, with an explicit error for `requires.permissions` pointing authors
to `gate` steps for approval. Docs and the model comment are updated to
state that `requires` is advisory, not a security boundary.
- Reject non-mapping `requires`, unknown keys, and `requires.permissions`
- Clarify workflows reference + PUBLISHING.md shell-step guidance
- Tests for valid keys, non-mapping, unknown key, and permissions
Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
Assisted-by: AI
* fix(workflows): address review feedback on requires validation
Follow-up to the review on #3079:
- Guard `requires` validation on `is not None` instead of truthiness so a
falsy non-mapping value (e.g. `requires: []` or `requires: ''`) is
reported as an error instead of being silently skipped; `requires:`
(YAML null) is still treated as an omitted block. Add a regression test.
- Reword the workflows security note so `requires.permissions` is shown
as rejected/unsupported rather than as a valid example of `requires`.
- Standardize on US spelling (`_RECOGNIZED_REQUIRES_KEYS`, "recognized")
to match the surrounding code and ease searching.
- Tighten the permissions-rejection test to assert on specific message
markers (`requires.permissions` and the `gate` guidance) so it fails if
the validation path or wording drifts.
Assisted-by: AI
Signed-off-by: Zied Jlassi (Architect AI) <6190550+zied-jlassi@users.noreply.github.com>
* fix(workflows): scope requires validation to workflow keys (drop tools/mcp)
tools and mcp belong to the bundle manifest requires schema (bundler/models/manifest.py, resolved in bundler/services/resolver.py), not the workflow requires validated here. Drop them from _RECOGNIZED_REQUIRES_KEYS and revert the PUBLISHING.md claim that this PR had introduced, so workflow requires only recognizes speckit_version and integrations.
This keeps the existing docs accurate and resolves the inline doc-consistency review comments.
Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
* refactor(workflows): type WorkflowDefinition.requires as Any pre-validation
self.requires holds the raw parsed value, which before validate_workflow()
runs may be a non-mapping (None for a bare 'requires:', a list for
'requires: []', etc.). Annotating it dict[str, Any] was misleading for
editors/type-checkers; use Any and document that validate_workflow() enforces
the mapping shape.
Addresses Copilot review feedback on engine.py.
Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
* fix(workflows): reject YAML-null requires: as a non-mapping
Address Copilot review: validate requires the same way as inputs. A
bare requires: parses as YAML null and was previously treated as an
omitted block, which is inconsistent with inputs and lets a stray
requires: line be silently ignored.
Drop the is-not-None guard and check isinstance(..., dict) directly: an
omitted block still defaults to {} (valid), but a present-but-non-mapping
value -- YAML null, [] or '' -- is now an authoring error that surfaces.
Tests: add YAML-null rejection + an omitted-is-still-valid guard test.
Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
---------
Signed-off-by: Zied Jlassi <6190550+zied-jlassi@users.noreply.github.com>
Signed-off-by: Zied Jlassi (Architect AI) <6190550+zied-jlassi@users.noreply.github.com>
The branch-name generator keeps a short (<3 char) word only when it
appears in uppercase in the description, treating it as an acronym (the
comment says as much). The bash script uses a case-sensitive grep for
this, but the PowerShell script used -match, which is case-insensitive
by default. As a result every short non-stop word was retained on
PowerShell even when lowercase, so the same description produced
different branch names across the two shells (e.g. 'go AI now' ->
001-go-ai-now on PS vs 001-ai-now on bash).
Switch to -cmatch so the check is case-sensitive and the two shells
agree. Adds parity tests covering a dropped lowercase short word and a
kept uppercase acronym.
* feat(integrations): add omp support
* Update updated_at timestamp
* refactor(integrations): delegate omp build_exec_args to base, register in issue templates
Inherit MarkdownIntegration.build_exec_args so omp picks up shared CLI
contract changes (requires_cli gating, extra-args ordering, --model
handling) automatically; only specialize the --mode json flag.
Also add Oh My Pi / omp to the issue-template agent lists so
test_issue_template_agent_lists_match_runtime_integrations passes.
* fix(integrations): use --print + positional prompt for omp argv
OMP's CLI parser treats `-p`/`--print` as a boolean (one-shot mode)
and consumes the prompt as a positional message; the previous
inherited `-p <prompt>` shape worked by accident only because `-p`
ignores its next token. Build the argv explicitly with flags first
and the prompt as a trailing positional, matching upstream args.ts.
render_toml_command() emitted the body inside a multiline *basic* TOML
string ("""..."""), which processes backslash escape sequences. A command
body containing a backslash — e.g. a Windows path like C:\Users\... whose
\U reads as an invalid unicode escape — therefore produced unparseable TOML
("Invalid hex value"), so the generated Gemini/Tabnine command file failed
to load. A body ending in a backslash also silently ate the closing newline
via TOML line-continuation.
Route bodies containing a backslash to the multiline *literal* form
('''...'''), which does not process escapes, or to the escaped basic string
when both triple-quote styles are present. Mirrors the escaping already done
by base.py's TomlIntegration.
Add tests covering a Windows path, a trailing backslash, and the
backslash + both-triple-quote-styles fallback.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
run_command() forwarded shell= straight to subprocess.run, so a caller
passing shell=True would invoke a shell. Reject shell=True with ValueError
(keeping the parameter for signature compatibility) and drop shell= from
both subprocess.run calls.
Enable ruff S602/S604/S605 to flag any future shell=True reintroduction,
annotate the one intentional workflow shell sink with # noqa: S602, and
document the shell-step execution risk in workflows/PUBLISHING.md.
* fix(scripts): send check-prerequisites.ps1 errors to stderr
The validation errors and run-hints in check-prerequisites.ps1 were
written with Write-Output, so they went to stdout. This script is
usually run with -Json and its stdout parsed by the agent, so an error
(e.g. missing plan.md) leaves the parser with an error string instead
of JSON. The bash counterpart already writes these to stderr (>&2), as
do the sibling PowerShell scripts (setup-tasks.ps1, common.ps1's
Get-FeaturePathsEnv). Switch the six error/hint lines to
[Console]::Error.WriteLine so stdout stays clean and the two shells
match.
* test(scripts): assert check-prerequisites errors stay on stderr
Per the #3122 bug assessment, tighten the failure-path tests so they
verify stdout stays clean (empty / valid JSON) and the error text only
appears on stderr, instead of checking the combined stdout+stderr
string. Covers all three PowerShell validation paths (missing feature
dir, missing plan.md, missing tasks.md with -RequireTasks) and the bash
counterpart. The two new error-routing tests fail on the pre-fix
script (errors on stdout) and pass after it.
* chore: bump version to 0.11.6
* chore: begin 0.11.7.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-23 09:47:17 -05:00
280 changed files with 30068 additions and 5394 deletions
description:Submit your bundle metadata for community catalog validation
title:"[Bundle]: Add "
labels:["enhancement","needs-triage"]
body:
- type:markdown
attributes:
value:|
Thanks for contributing a bundle! This template captures metadata for maintainers to validate formatting, links, component resolution, and installation evidence. Maintainers do not audit, endorse, or support bundle code or installed components.
**Before submitting:**
- Review the [Bundles reference](https://github.com/github/spec-kit/blob/main/docs/reference/bundles.md)
- Ensure your bundle has a valid `bundle.yml` manifest
- Create a GitHub release with a versioned bundle artifact
- Test installation from a downloaded artifact: `specify bundle install ./your-bundle-1.0.0.zip`
- If you host a bundle catalog, test catalog installation with `specify bundle catalog add <catalog-url> --id <catalog-id> --policy install-allowed` and `specify bundle install <bundle-id>`
- If your bundle depends on components from non-default catalogs, document those catalog URLs and test installation from a clean project
- type:input
id:bundle-id
attributes:
label:Bundle ID
description:Unique bundle identifier; must start and end with a lowercase letter or digit and may contain lowercase letters, digits, dots, underscores, and hyphens between
placeholder:"e.g., security-governance-stack"
validations:
required:true
- type:input
id:bundle-name
attributes:
label:Bundle Name
description:Human-readable bundle name
placeholder:"e.g., Security Governance Stack"
validations:
required:true
- type:input
id:version
attributes:
label:Version
description:Semantic version number
placeholder:"e.g., 1.0.0"
validations:
required:true
- type:input
id:role
attributes:
label:Role or Team
description:Primary role, team, or persona this bundle provisions
description:List any non-default catalogs users must add before this bundle can resolve its components; enter "None" if every component resolves from built-in or bundled catalogs
description:Confirm that your bundle has been tested
options:
- label:Validation succeeds with `specify bundle validate --path <bundle-directory>`
required:true
- label:Build succeeds with `specify bundle build --path <bundle-directory>` and produces the submitted artifact
required:true
- label:Bundle installs successfully from the built artifact
required:true
- label:The submitted distribution path was tested end to end, including bundle-ID installation from an install-allowed catalog when a catalog entry is proposed
required:true
- label:Installation was tested in a clean Spec Kit project
required:true
- label:Required component catalogs are documented and were included in testing, or no extra catalogs are required
required:true
- label:Documentation is complete and accurate
required:true
- type:checkboxes
id:requirements
attributes:
label:Submission Requirements
description:Verify your bundle meets all requirements
options:
- label:Valid `bundle.yml` manifest included
required:true
- label:README.md explains the bundle's intended role, installed components, and installation steps
required:true
- label:LICENSE file included
required:true
- label:GitHub release created with a version tag
required:true
- label:Bundle ID matches the manifest and follows naming conventions
required:true
- label:Every extension, preset, workflow, and step reference is pinned where the manifest requires a version
required:true
- type:textarea
id:testing-details
attributes:
label:Testing Details
description:Describe how you tested your bundle
placeholder:|
**Tested on:**
- macOS 15 with Spec Kit v0.9.0
- Ubuntu 24.04 with Spec Kit v0.9.0
**Test project:** [Link or description]
**Test scenarios:**
1. Added required catalogs
2. Validated bundle manifest
3. Built release artifact
4. Installed bundle in a clean project
5. Ran the installed commands or workflows
validations:
required:true
- type:textarea
id:example-usage
attributes:
label:Example Usage
description:Provide a simple example of installing and using your bundle
Thanks for contributing a preset! This template helps you submit your preset to the community catalog.
**Before submitting:**
- Review the [Preset Publishing Guide](https://github.com/github/spec-kit/blob/main/presets/PUBLISHING.md)
- Ensure your preset has a valid `preset.yml` manifest
@@ -77,6 +77,18 @@ body:
validations:
required:true
- type:input
id:documentation
attributes:
label:Documentation URL
description:|
Link to the README that explains how to use **this preset** (not a general product/framework pitch).
Prefer the preset-scoped README (e.g. `presets/<id>/README.md` in a monorepo) over the repository root README.
It must contain at least one valid `specify preset add ...` install command — ideally `specify preset add --from <download-url>` using the exact Download URL above (other forms such as `specify preset add <preset-id>` or `specify preset add --dev <path>` are also accepted).
- label:README.md with description and usage instructions
- label:Linked README (Documentation URL) explains how to use this preset and includes a valid `specify preset add ...` command (preferably `specify preset add --from <download-url>` using the exact download URL)
# This file was automatically generated by gh-aw (v0.79.8). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
# Days of inactivity before a stale issue or PR is closed (after being marked stale)
days-before-close:30
# Stale issue settings
stale-issue-message:'This issue has been automatically marked as stale because it has not had any activity for 150 days. It will be closed in 30 days if no further activity occurs.'
close-issue-message:'This issue has been automatically closed due to inactivity (180 days total). If you believe this issue is still relevant, please reopen it or create a new issue.'
stale-issue-label:'stale'
# Stale PR settings
stale-pr-message:'This pull request has been automatically marked as stale because it has not had any activity for 150 days. It will be closed in 30 days if no further activity occurs.'
close-pr-message:'This pull request has been automatically closed due to inactivity (180 days total). If you believe this PR is still relevant, please reopen it or create a new PR.'
stale-pr-label:'stale'
# Exempt issues and PRs with these labels from being marked as stale
exempt-issue-labels:'pinned,security'
exempt-pr-labels:'pinned,security'
# Only issues or PRs with all of these labels are checked
@@ -52,30 +52,29 @@ Most agents only need `MarkdownIntegration` — a minimal subclass with zero met
Create `src/specify_cli/integrations/<package_dir>/__init__.py`, where `<package_dir>` is the Python-safe directory name derived from `<key>`: use the key as-is when it contains no hyphens (e.g., key `"gemini"` → `gemini/`), or replace hyphens with underscores when it does (e.g., key `"kiro-cli"` → `kiro_cli/`). The `IntegrationBase.key` class attribute always retains the original hyphenated value, since that is what the CLI and registry use. For CLI-based integrations (`requires_cli: True`), the `key` should match the actual CLI tool name (the executable users install and run) so CLI checks can resolve it correctly. For IDE-based integrations (`requires_cli: False`), use the canonical integration identifier instead.
**Minimal example — Markdown agent (Windsurf):**
**Minimal example — Markdown agent (Kilo Code):**
```python
"""Windsurf IDE integration."""
"""Kilo Code IDE integration."""
from..baseimportMarkdownIntegration
classWindsurfIntegration(MarkdownIntegration):
key="windsurf"
classKilocodeIntegration(MarkdownIntegration):
key="kilocode"
config={
"name":"Windsurf",
"folder":".windsurf/",
"name":"Kilo Code",
"folder":".kilocode/",
"commands_subdir":"workflows",
"install_url":None,
"requires_cli":False,
}
registrar_config={
"dir":".windsurf/workflows",
"dir":".kilocode/workflows",
"format":"markdown",
"args":"$ARGUMENTS",
"extension":".md",
}
context_file=".windsurf/rules/specify-rules.md"
```
**TOML agent (Gemini):**
@@ -101,7 +100,6 @@ class GeminiIntegration(TomlIntegration):
"args":"{{args}}",
"extension":".toml",
}
context_file="GEMINI.md"
```
**Skills agent (Codex):**
@@ -129,7 +127,6 @@ class CodexIntegration(SkillsIntegration):
"args":"$ARGUMENTS",
"extension":"/SKILL.md",
}
context_file="AGENTS.md"
@classmethod
defoptions(cls)->list[IntegrationOption]:
@@ -150,9 +147,8 @@ class CodexIntegration(SkillsIntegration):
| `key` | Class attribute | Unique identifier; for CLI-based integrations (`requires_cli: True`), must match the CLI executable name |
| `context_file` | Class attribute (str or None) | Path to agent context/instructions file (e.g., `"CLAUDE.md"`, `".github/copilot-instructions.md"`) |
**Key design rule:** For CLI-based integrations (`requires_cli: True`), `key` must be the actual executable name (e.g., `"cursor-agent"` not `"cursor"`). This ensures `shutil.which(key)` works for CLI-tool checks without special-case mappings. IDE-based integrations (`requires_cli: False`) should use their canonical identifier (e.g., `"windsurf"`, `"copilot"`).
**Key design rule:** For CLI-based integrations (`requires_cli: True`), `key` must be the actual executable name (e.g., `"cursor-agent"` not `"cursor"`). This ensures `shutil.which(key)` works for CLI-tool checks without special-case mappings. IDE-based integrations (`requires_cli: False`) should use their canonical identifier (e.g., `"kilocode"`, `"copilot"`).
Set `context_file` on the integration class. The base integration setup creates or updates the managed Spec Kit section in that file, and uninstall removes the managed section when appropriate.
The Specify CLI carries **no agent-context state whatsoever**. Integration classes do **not** declare a `context_file`, and the CLI never creates, updates, removes, resolves, or migrates a context/instruction file (`CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`, …). New integrations add nothing for context handling.
The managed section is owned by the bundled `agent-context` extension (`extensions/agent-context/`). All configuration flows through the extension's own config file at `.specify/extensions/agent-context/agent-context-config.yml`:
Managing the "Spec Kit" section in the context file is fully owned by the bundled `agent-context` extension (`extensions/agent-context/`), which is a **full opt-in**: `specify init` does not install it. A user adds/enables it through the standard extension verbs, after which the extension's own bundled scripts maintain the context section. When the extension is absent or disabled, nothing in Spec Kit touches the context file.
The extension reads its own config file at `.specify/extensions/agent-context/agent-context-config.yml`:
```yaml
# Path to the coding agent context file managed by this extension
@@ -189,10 +187,10 @@ context_markers:
end:"<!-- SPECKIT END -->"
```
-`context_file` is written automatically from the integration's class attribute when `specify init` or `specify integration use` is run.
-`context_markers.{start,end}`defaults to `IntegrationBase.CONTEXT_MARKER_START` / `CONTEXT_MARKER_END`. Users who want custom markers edit `agent-context-config.yml` directly — both the Python layer (`upsert_context_section()` / `remove_context_section()`) and the bundled scripts (`extensions/agent-context/scripts/bash/update-agent-context.sh` and `.ps1`) read from this single source of truth.
-The Specify CLI does **not** write this config. When `context_file` is empty, the extension's bundled scripts self-seed it by looking up the active integration's key in the extension's own `agent-context-defaults.json` map (`extensions/agent-context/scripts/bash/update-agent-context.sh` and `.ps1`). The CLI registry is never consulted — all agent→context-file knowledge lives inside the extension.
-`context_markers.{start,end}`are read solely by the extension's scripts; they default to the Spec Kit markers shown above and can be customized by editing `agent-context-config.yml` directly.
Users can opt out entirely with `specify extension disable agent-context`; while disabled, Spec Kit skips context-file creation, updates, and removal (the gates are inside `upsert_context_section()` and `remove_context_section()`).
Existing projects created by older Spec Kit versions keep working: any previously written managed section or extension config is left intact and is only ever updated by the extension when run.
Only add custom setup logic when the agent needs non-standard behavior. Integrations no longer require per-agent thin wrapper scripts or shared context-update dispatcher scripts — the `agent-context` extension is fully generic.
@@ -203,8 +201,8 @@ Only add custom setup logic when the agent needs non-standard behavior. Integrat
specify init my-project --integration <key>
# Verify files were created in the commands directory configured by
# config["folder"] + config["commands_subdir"] (for example, .windsurf/workflows/)
ls -R my-project/.windsurf/workflows/
# config["folder"] + config["commands_subdir"] (for example, .kilocode/workflows/)
ls -R my-project/.kilocode/workflows/
# Uninstall cleanly
cd my-project && specify integration uninstall <key>
@@ -401,7 +399,6 @@ Implementation: Extends `YamlIntegration` (parallel to `TomlIntegration`):
2. Extracts title and description from frontmatter
4. Uses `yaml.safe_dump()` for header fields to ensure proper escaping
5. Sets `context_file = "AGENTS.md"` so the base setup manages the Spec Kit context section there
## Branch Naming Convention
@@ -466,7 +463,7 @@ Disclosure is **continuous**, not a one-time event. A single AI-disclosure parag
## Common Pitfalls
1. **Using shorthand keys for CLI-based integrations**: For CLI-based integrations (`requires_cli: True`), the `key` must match the executable name (e.g., `"cursor-agent"` not `"cursor"`). `shutil.which(key)` is used for CLI tool checks — mismatches require special-case mappings. IDE-based integrations (`requires_cli: False`) are not subject to this constraint.
2. **Forgetting context configuration**: The bundled `agent-context` extension reads from `.specify/extensions/agent-context/agent-context-config.yml`. New integrations only need to set `context_file` on the class — markers and dispatcher scripts are managed centrally.
2. **Reintroducing context handling into the CLI**: The opt-in `agent-context` extension owns everything about context files — including the per-agent default mapping in `agent-context-defaults.json`. Integration classes must **not** declare a `context_file`, and no CLI code should read, write, resolve, or migrate context files. All context-file logic lives in `.specify/extensions/agent-context/` and its bundled scripts.
3. **Incorrect `requires_cli` value**: Set to `True` only for agents that have a CLI tool; set to `False` for IDE-based agents.
4. **Wrong argument format**: Use `$ARGUMENTS` for Markdown agents, `{{args}}` for TOML agents.
5. **Skipping registration**: The import and `_register()` call in `_register_builtins()` must both be added.
@@ -45,7 +45,7 @@ Spec-Driven Development **flips the script** on traditional software development
### 1. Install Specify CLI
Requires **[uv](https://docs.astral.sh/uv/)** ([install uv](./docs/install/uv.md)). Replace `vX.Y.Z` with the latest tag from [Releases](https://github.com/github/spec-kit/releases):
Requires **[uv](https://docs.astral.sh/uv/)** ([install uv](./docs/install/uv.md)). Replace `vX.Y.Z` with the latest release tag from [Releases](https://github.com/github/spec-kit/releases) — keep the leading `v` (for example, `v0.12.11`, not `0.12.11`):
- [Friends](https://github.github.io/spec-kit/community/friends.html) — projects that extend or build on Spec Kit
> [!NOTE]
> Community contributions are independently created and maintained by their respective authors. Review source code before installation and use at your own discretion.
Want to contribute? See the [Extension Publishing Guide](extensions/EXTENSION-PUBLISHING-GUIDE.md) or the [Presets Publishing Guide](presets/PUBLISHING.md).
Want to contribute? See the [Extension Publishing Guide](extensions/EXTENSION-PUBLISHING-GUIDE.md), the [Presets Publishing Guide](presets/PUBLISHING.md), or the [Community Bundles guide](docs/community/bundles.md).
## 🤖 Supported AI Coding Agent Integrations
@@ -262,8 +263,10 @@ built-in). Each source carries an install policy: `install-allowed` sources can
be installed from, while `discovery-only` sources are visible in `search`/`info`
but refuse installation. Manage the stack with `specify bundle catalog list|add|remove`.
Authors validate and package bundles locally — there is no first-class publish;
distribution is hosting the built artifact and adding a catalog entry:
Authors validate and package bundles locally. Distribution is hosting the built
artifact and adding a catalog source; community bundle submissions use the
The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, Codex CLI, Qoder CLI, Tabnine CLI, Kiro CLI, Pi, Forge, Goose, Mistral Vibe, or ZCode installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
The CLI checks that the selected integration's required CLI tool is installed on your machine when that integration has `requires_cli: True`. If you do not have the required tool installed, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
> Community bundles are independently created and maintained by their respective authors. Maintainers only verify that submission metadata is complete and correctly formatted — they do **not review, audit, endorse, or support the bundle code or the components it installs**. Review bundle manifests, component catalogs, and source repositories before installation and use at your own discretion.
Bundles compose existing Spec Kit components — extensions, presets, workflows, and steps — into a single role or team stack. They are useful when a user should be able to install a tested set of components together instead of following several separate install commands.
Accepted community bundle entries will be listed here once a community bundle catalog is available. To submit a bundle for review, file a [Bundle Submission](https://github.com/github/spec-kit/issues/new?template=bundle_submission.yml) issue.
## What to Submit
A bundle submission should include:
- A public repository with a valid `bundle.yml` manifest.
- A versioned GitHub release with a bundle artifact created by `specify bundle build`.
- Documentation that explains the intended role, installed components, required catalogs, and expected workflow.
- A proposed catalog entry with bundle metadata and component counts.
- Test evidence from a clean Spec Kit project.
## Component Resolution
A bundle catalog entry describes where to download the bundle artifact, but the bundle's component references still need to resolve when a user installs it. References can resolve from bundled components, already installed components, or active extension, preset, workflow, and step catalogs.
If your bundle depends on components that are not available from the default Spec Kit catalogs, include the required catalog URLs in the submission and in your README. Test the full install path from a clean project with those catalogs added before submitting.
- The submission fields are complete and correctly formatted.
- The release artifact and documentation URLs are reachable.
- The repository contains a `bundle.yml` manifest.
- The submission clearly identifies any required component catalogs.
- The proposed catalog entry uses the expected bundle catalog entry shape.
Maintainers do not audit the behavior of installed extensions, presets, workflows, steps, or scripts. Users should review those components before installing a community bundle.
## Updating a Bundle
To update a submitted bundle, file another [Bundle Submission](https://github.com/github/spec-kit/issues/new?template=bundle_submission.yml) issue with the new version, download URL, changed component list, and updated test evidence. Mention that the issue updates an existing bundle entry.
| AI-Driven Engineering (AIDE) | A structured 7-step workflow for building new projects from scratch with AI assistants — from vision through implementation | `process` | Read+Write | [aide](https://github.com/mnriem/spec-kit-extensions/tree/main/aide) |
| Analytics | Measure what your AI builds, and how much time it saves you | `visibility` | Read+Write | [spec-kit-analytics](https://github.com/Fyloss/spec-kit-analytics) |
| API Evolve | Managed API contract evolution — breaking-change detection, semver enforcement, deprecation orchestration, and lifecycle gates across REST, GraphQL, and gRPC | `process` | Read+Write | [spec-kit-api-evolve](https://github.com/Quratulain-bilal/spec-kit-api-evolve) |
| Architect Impact Previewer | Predicts architectural impact, complexity, and risks of proposed changes before implementation. | `visibility` | Read-only | [spec-kit-architect-preview](https://github.com/UmmeHabiba1312/spec-kit-architect-preview) |
| Architecture Guard | Framework-agnostic architecture review extension for validating implementation against governance and architecture constitutions, detecting architectural drift, and generating non-blocking refactor tasks | `process` | Read+Write | [spec-kit-architecture-guard](https://github.com/DyanGalih/spec-kit-architecture-guard) |
| Architecture Workflow | Generate or reverse project-level 4+1 architecture views as separate commands | `docs` | Read+Write | [spec-kit-arch](https://github.com/bigsmartben/spec-kit-arch) |
| Architecture Workflow | Generate or reverse project-level 4+1 architecture views with per-view and full-workflow commands | `docs` | Read+Write | [spec-kit-arch](https://github.com/bigsmartben/spec-kit-arch) |
| Archive Extension | Archive merged features into main project memory. | `docs` | Read+Write | [spec-kit-archive](https://github.com/stn1slv/spec-kit-archive) |
| Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | `integration` | Read+Write | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) |
| Blueprint | Stay code-literate in AI-driven development: review a complete code blueprint for every task from spec artifacts before /speckit.implement runs | `docs` | Read+Write | [spec-kit-blueprint](https://github.com/chordpli/spec-kit-blueprint) |
@@ -41,6 +42,7 @@ The following community-contributed extensions are available in [`catalog.commun
| Catalog CI | Automated validation for spec-kit community catalog entries — structure, URLs, diffs, and linting | `process` | Read-only | [spec-kit-catalog-ci](https://github.com/Quratulain-bilal/spec-kit-catalog-ci) |
| Charter | Compose modular project constitutions from shared fragment registries. Centralize governance rules, select per-project fragments, track upstream changes, and keep multi-project setups consistent. | `process` | Read+Write | [spec-kit-charter](https://github.com/Fyloss/spec-kit-charter) |
| CI Guard | Spec compliance gates for CI/CD — verify specs exist, check drift, and block merges on gaps | `process` | Read-only | [spec-kit-ci-guard](https://github.com/Quratulain-bilal/spec-kit-ci-guard) |
| Checkpoint Extension | Commit the changes made during the middle of the implementation, so you don't end up with just one very large commit at the end | `code` | Read+Write | [spec-kit-checkpoint](https://github.com/aaronrsun/spec-kit-checkpoint) |
| Cleanup Extension | Post-implementation quality gate that reviews changes, fixes small issues (scout rule), creates tasks for medium issues, and generates analysis for large issues | `code` | Read+Write | [spec-kit-cleanup](https://github.com/dsrednicki/spec-kit-cleanup) |
@@ -49,21 +51,24 @@ The following community-contributed extensions are available in [`catalog.commun
| Confluence Extension | Create a doc in Confluence summarizing the specifications and planning files | `integration` | Read+Write | [spec-kit-confluence](https://github.com/aaronrsun/spec-kit-confluence) |
| Cost Tracker | Track real LLM dollar cost across SDD workflows — per-feature budgets, per-integration comparison, and finance-ready exports | `visibility` | Read+Write | [spec-kit-cost](https://github.com/Quratulain-bilal/spec-kit-cost) |
| Data Model Diagram | Generates Mermaid ER diagrams from Spec Kit data models after planning | `docs` | Read+Write | [spec-kit-data-model-diagram](https://github.com/benizzio/spec-kit-data-model-diagram) |
| DocGuard — CDD Enforcement | Canonical-Driven Development enforcement. Validates, scores, and traces project documentation with automated checks, AI-driven workflows, and spec-kit hooks. One pinned runtime dependency; pure Node.js otherwise. | `docs` | Read+Write | [spec-kit-docguard](https://github.com/raccioly/docguard) |
| DocGuard — CDD Enforcement | Doc-integrity engine with MCP server, SARIF output, and zero-LLM core. Validates, scores, and traces documentation against code — 24 validators, stable finding codes, spec-kit hooks. Pure Node.js. | `docs` | Read+Write | [spec-kit-docguard](https://github.com/raccioly/docguard) |
| EARS Requirements Syntax | Author, lint, and convert requirements using EARS - the five industry-standard sentence patterns for unambiguous, testable requirements | `docs` | Read+Write | [spec-kit-ears](https://github.com/dhruv-15-03/spec-kit-ears) |
| Extensify | Create and validate extensions and extension catalogs | `process` | Read+Write | [extensify](https://github.com/mnriem/spec-kit-extensions/tree/main/extensify) |
| Fix Findings | Automated analyze-fix-reanalyze loop that resolves spec findings until clean | `code` | Read+Write | [spec-kit-fix-findings](https://github.com/Quratulain-bilal/spec-kit-fix-findings) |
| Fleet Orchestrator | Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases | `process` | Read+Write | [spec-kit-fleet](https://github.com/sharathsatish/spec-kit-fleet) |
| GitHub Issues Integration 2 | Creates and syncs local specs from an existing GitHub issue | `integration` | Read+Write | [spec-kit-issue](https://github.com/aaronrsun/spec-kit-issue) |
| Golden Demo | Deterministic behavioral drift oracle. Extracts acceptance criteria, generates fuzz test vectors (seed=42), compares golden Python implementations against real code in any language. CI/CD gatekeeper with warn/strict modes. | `docs` | Read+Write | [spec-kit-golden-demo](https://github.com/jasstt/spec-kit-golden-demo) |
| Improve Extension | Audits any codebase as a senior advisor and writes prioritized, self-contained spec prompts under specs/ that the spec-kit lifecycle can process | `process` | Read+Write | [spec-kit-improve](https://github.com/d0whc3r/spec-kit-improve) |
| Intake | Normalize PRD, design, and test-case evidence into SDD-ready intake artifacts | `docs` | Read+Write | [spec-kit-intake](https://github.com/bigsmartben/spec-kit-intake) |
| Intake | Normalize PRD, design, HTML SSOT, and test-case evidence into SDD-ready intake artifacts. | `docs` | Read+Write | [spec-kit-intake](https://github.com/bigsmartben/spec-kit-intake) |
| Iterate | Iterate on spec documents with a two-phase define-and-apply workflow — refine specs mid-implementation and go straight back to building | `docs` | Read+Write | [spec-kit-iterate](https://github.com/imviancagrace/spec-kit-iterate) |
| Jira Integration | Create Jira Epics, Stories, and Issues from spec-kit specifications and task breakdowns with configurable hierarchy and custom field support | `integration` | Read+Write | [spec-kit-jira](https://github.com/mbachorik/spec-kit-jira) |
| Jira Integration (Sync Engine) | Idempotent, drift-aware, fail-closed reconcile engine mirroring spec-kit specs into Jira (Epic per repo, Story per spec, Subtask per phase) | `integration` | Read+Write | [spec-kit-jira-sync](https://github.com/ashbrener/spec-kit-jira-sync) |
| Learning Extension | Generate educational guides from implementations and enhance clarifications with mentoring context | `docs` | Read+Write | [spec-kit-learn](https://github.com/imviancagrace/spec-kit-learn) |
| Linear Integration | Mirror spec-kit feature directories into Linear (filesystem → Linear, reconcile-based, unidirectional). | `integration` | Read+Write | [spec-kit-linear-sync](https://github.com/ashbrener/spec-kit-linear-sync) |
| MAQA Azure DevOps Integration | Azure DevOps Boards integration for MAQA — syncs User Stories and Task children as features progress | `integration` | Read+Write | [spec-kit-maqa-azure-devops](https://github.com/GenieRobot/spec-kit-maqa-azure-devops) |
@@ -79,10 +84,12 @@ The following community-contributed extensions are available in [`catalog.commun
| Microsoft 365 Integration | Fetch Teams messages, meeting transcripts, and SharePoint/OneDrive files as local Markdown for spec generation | `integration` | Read+Write | [spec-kit-m365](https://github.com/BenBtg/spec-kit-m365) |
| Multi-Repo Branch Sync | Creates the feature branch in affected sub-repositories and git submodules via plan/tasks hooks | `process` | Read+Write | [multi-repo-sync](https://github.com/fyloss/spec-kit-multi-repo-sync) |
| Multi-Sites Spec Kit | Multi-site aware specify command with per-site spec folders, auto-increment, and Drupal support | `process` | Read+Write | [spec-kit-multi-sites](https://github.com/teeyo/spec-kit-multi-sites) |
| .NET Framework to Modern .NET Migration | Orchestrate end-to-end .NET Framework to modern .NET migration across 7 phases, with SDD lifecycle integration | `process` | Read+Write | [spec-kit-fx-to-net](https://github.com/RogerBestMsft/spec-kit-FxToNet) |
| Onboard | Contextual onboarding and progressive growth for developers new to spec-kit projects. Explains specs, maps dependencies, validates understanding, and guides the next step | `process` | Read+Write | [spec-kit-onboard](https://github.com/dmux/spec-kit-onboard) |
| Optimize | Audit and optimize AI governance for context efficiency — token budgets, rule health, interpretability, compression, coherence, and echo detection | `process` | Read+Write | [spec-kit-optimize](https://github.com/sakitA/spec-kit-optimize) |
| OWASP LLM Threat Model | OWASP Top 10 for LLM Applications 2025 threat analysis on agent artifacts | `code` | Read-only | [spec-kit-threatmodel](https://github.com/NaviaSamal/spec-kit-threatmodel) |
| Plan Review Gate | Require spec.md and plan.md to be merged via MR/PR before allowing task generation | `process` | Read-only | [spec-kit-plan-review-gate](https://github.com/luno/spec-kit-plan-review-gate) |
@@ -92,17 +99,19 @@ The following community-contributed extensions are available in [`catalog.commun
| Project Health Check | Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git | `visibility` | Read-only | [spec-kit-doctor](https://github.com/KhawarHabibKhan/spec-kit-doctor) |
| Project Status | Show current SDD workflow progress — active feature, artifact status, task completion, workflow phase, and extensions summary | `visibility` | Read-only | [spec-kit-status](https://github.com/KhawarHabibKhan/spec-kit-status) |
| QA Testing Extension | Systematic QA testing with browser-driven or CLI-based validation of acceptance criteria from spec | `code` | Read-only | [spec-kit-qa](https://github.com/arunt14/spec-kit-qa) |
| Quality Gates (Enforcement Layer) | Deterministic quality enforcement for Spec Kit projects at three boundaries — agent hooks, git pre-commit, CI — with one policy file and provable enforcement (attestations, canaries, verified parity). | `process` | Read+Write | [spec-gates](https://github.com/schwichtgit/spec-gates) |
| RAG Azure Builder | Spec Kit extension for onboarding and operating an Azure RAG stack with guided workflows. | `process` | Read+Write | [spec-kit-extension-rag-azure-builder](https://github.com/Sertxito/spec-kit-extension-rag-azure-builder) |
| Ralph Loop | Autonomous implementation loop using AI agent CLI | `code` | Read+Write | [spec-kit-ralph](https://github.com/Rubiss-Projects/spec-kit-ralph) |
| Red Team | Adversarial review of specs before /speckit.plan — parallel lens agents surface risks that clarify/analyze structurally can't (prompt injection, integrity gaps, cross-spec drift, silent failures). Produces a structured findings report; no auto-edits to specs. | `docs` | Read+Write | [spec-kit-red-team](https://github.com/ashbrener/spec-kit-red-team) |
| Research Harness | State-externalizing research harness: budgeted exploration, evidence curation, and claim verification for spec-driven development | `process` | Read+Write | [spec-kit-harness](https://github.com/formin/spec-kit-harness) |
| Repository Index | Generate index for existing repo for overview, architecture and module level. | `docs` | Read-only | [spec-kit-repoindex](https://github.com/liuyiyu/spec-kit-repoindex) |
| Spec Kit Memory | Recalls prior specs and decisions from configurable memory tools (e.g. memsearch) before SDLC stages, so planning and specification start from what the project already knows | `docs` | Read+Write | [spec-kit-memory](https://github.com/zaytsevand/spec-kit-memory) |
| Spec Kit Preview | Generate evidence-backed low, mid, or high fidelity previews from Spec Kit artifacts as Markdown or self-contained HTML | `docs` | Read+Write | [spec-kit-preview](https://github.com/bigsmartben/spec-kit-preview) |
| Spec Kit Schedule | Optimal multi-agent task scheduling via CP-SAT — DAG precedence, hallucination-aware caps, file-conflict avoidance, stochastic durations, replanning, and interactive HTML output | `process` | Read+Write | [spec-kit-schedule](https://github.com/jfranc38/spec-kit-schedule) |
| Spec Kit TLDR | Render a feature's spec.md / plan.md into a review-oriented TLDR (self-contained HTML dashboard + PR-native Markdown) that surfaces risks for faster PR review. | `visibility` | Read+Write | [speckit-tldr](https://github.com/qurore/speckit-tldr) |
| Spec Reference Loader | Reads the ## References section from the feature spec and loads only the listed docs into context | `docs` | Read-only | [spec-kit-spec-reference-loader](https://github.com/KevinBrown5280/spec-kit-spec-reference-loader) |
| Spec Refine | Update specs in-place, propagate changes to plan and tasks, and diff impact across artifacts | `process` | Read+Write | [spec-kit-refine](https://github.com/Quratulain-bilal/spec-kit-refine) |
| Spec Roadmap | Capture a durable spec roadmap after the constitution, then review specs against it before and after implementation so spec-specific decisions, outcomes, and constraints are never lost. | `process` | Read+Write | [speckit-roadmap](https://github.com/srobroek/speckit-roadmap) |
| Spec Scope | Effort estimation and scope tracking — estimate work, detect creep, and budget time per phase | `process` | Read-only | [spec-kit-scope-](https://github.com/Quratulain-bilal/spec-kit-scope-) |
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | `docs` | Read+Write | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
| Spec Trace | Build a requirement → test traceability matrix from spec.md and the test suite — surface untested requirements and orphan tests | `code` | Read+Write | [spec-kit-trace](https://github.com/Quratulain-bilal/spec-kit-trace) |
@@ -139,6 +151,7 @@ The following community-contributed extensions are available in [`catalog.commun
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | `docs` | Read+Write | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
| Verify Tasks Extension | Detect phantom completions: tasks marked [X] in tasks.md with no real implementation | `code` | Read-only | [spec-kit-verify-tasks](https://github.com/datastone-inc/spec-kit-verify-tasks) |
| Version Guard | Verify tech stack versions against live npm registries before planning and implementation | `process` | Read-only | [spec-kit-version-guard](https://github.com/KevinBrown5280/spec-kit-version-guard) |
| What-if Analysis | Preview the downstream impact (complexity, effort, tasks, risks) of requirement changes before committing to them | `visibility` | Read-only | [spec-kit-whatif](https://github.com/DevAbdullah90/spec-kit-whatif) |
@@ -7,8 +7,12 @@ Community projects that extend, visualize, or build on Spec Kit:
- **[cc-spex](https://github.com/rhuss/cc-spex)** — A Claude Code plugin that adds composable traits on top of Spec Kit with [Superpowers](https://github.com/obra/superpowers)-based quality gates, spec/code review, git worktree isolation, and parallel implementation via agent teams.
- **[Spec Kit Assistant](https://marketplace.visualstudio.com/items?itemName=rfsales.speckit-assistant)** — A VS Code extension that provides a visual orchestrator for the full SDD workflow (constitution → specification → planning → tasks → implementation) with phase status visualization, an interactive task checklist, DAG visualization, and support for Claude, Gemini, GitHub Copilot, and OpenAI backends. Requires the `specify` CLI in your PATH.
- **[VS Code Spec Kit Assistant](https://marketplace.visualstudio.com/items?itemName=rfsales.speckit-assistant)** — A VS Code extension that provides a visual orchestrator for the full SDD workflow (constitution → specification → planning → tasks → implementation) with phase status visualization, an interactive task checklist, DAG visualization, and support for Claude, Gemini, GitHub Copilot, and OpenAI backends. Requires the `specify` CLI in your PATH.
- **[SpecKit Assistant](https://www.npmjs.com/package/speckit-assistant)** — A visual orchestrator for Spec-Driven Development (SDD). It connects your local specification, planning, and task checklists with AI agents (Claude, Gemini, GitHub Copilot). No global installation required — just run it via `npx speckit-assistant`.
- **[SpecKit Companion](https://marketplace.visualstudio.com/items?itemName=alfredoperez.speckit-companion)** — A VS Code extension that brings a visual GUI to Spec Kit. Browse specs in a rich markdown viewer with clickable file references, create specifications with image attachments, comment and refine each step inline (GitHub-style review), track your progress through the SDD workflow with a visual phase stepper, and manage steering documents like constitutions and templates.
- **[cc-spec-kit](https://github.com/speckit-community/cc-spec-kit)** — Community-maintained plugin for Claude Code and GitHub Copilot CLI that installs Spec Kit skills via the plugin marketplace.
- **[spectatui](https://github.com/tinesoft/spectatui)** — A terminal UI (TUI) dashboard for Spec Kit that lets you track features, manage specifications, integrations, presets, workflows, and extensions, and monitor AI agent workflows. Attach to existing AI sessions or launch new ones from your terminal. Keyboard and mouse support. Light/dark theme support. Customizable and performance-oriented. Requires the `specify` CLI in your PATH.
The Spec Kit community builds extensions, presets, walkthroughs, and companion projects that expand what you can do with Spec-Driven Development. All community contributions are independently created and maintained by their respective authors.
The Spec Kit community builds extensions, presets, bundles, walkthroughs, and companion projects that expand what you can do with Spec-Driven Development. All community contributions are independently created and maintained by their respective authors.
## Extensions
@@ -14,6 +14,12 @@ Presets customize how Spec Kit behaves — overriding templates, commands, and t
[Browse community presets →](presets.md)
## Bundles
Bundles compose extensions, presets, workflows, and steps into role or team stacks that can be installed together.
[Browse community bundles →](bundles.md)
## Walkthroughs
Step-by-step guides that show Spec-Driven Development in action across different scenarios, languages, and frameworks.
| Canon Core | Adapts original Spec Kit workflow to work together with Canon extension | 2 templates, 8 commands | — | [spec-kit-canon](https://github.com/maximiliamus/spec-kit-canon) |
| Claude AskUserQuestion | Upgrades `/speckit.clarify` and `/speckit.checklist` on Claude Code from Markdown-table prompts to the native AskUserQuestion picker, with a recommended option and reasoning on every question | 2 commands | — | [spec-kit-preset-claude-ask-questions](https://github.com/0xrafasec/spec-kit-preset-claude-ask-questions) |
| Command Density | Compacts the nine core Spec Kit command prompts while preserving scripts, handoffs, placeholders, hook output blocks, and rule structure | 9 commands | — | [spec-kit-preset-command-density](https://github.com/Xopoko/spec-kit-preset-command-density) |
@@ -25,9 +26,10 @@ The following community-contributed presets customize how Spec Kit behaves — o
| Pirate Speak (Full) | Transforms all Spec Kit output into pirate speak — specs become "Voyage Manifests", plans become "Battle Plans", tasks become "Crew Assignments" | 6 templates, 9 commands | — | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
| Screenwriting | Spec-Driven Development for screenwriting/scriptwriting/tutorials: feature films, television (pilot, episode, limited series), and stage plays. Adapts the Spec Kit workflow to screenplay craft — slug lines, action lines, act breaks, beat sheets, and industry-standard pitch documents. Supports three-act, Save the Cat, TV pilot, network episode, cable/streaming episode, and stage-play structural frameworks. Export to Fountain, FTX, PDF | 26 templates, 32 commands, 1 script | — | [speckit-preset-screenwriting](https://github.com/adaumann/speckit-preset-screenwriting) |
| Security Governance | Adds memory-safe-language preference, language-specific secure coding profiles, audit-ready Spec-Kit run evidence, ASVS verification, SBOM/AI-SBOM supply-chain transparency, CRA awareness, and regulatory applicability screening for NIS2, CRA, EU AI Act, and DORA | 14 templates, 3 commands | — | [spec-kit-preset-security-governance](https://github.com/hindermath/spec-kit-preset-security-governance) |
| SicarioSpec Core | Evidence-first security operations governance that maps feature risk to controls, gates, evidence, owners, approval, and accepted-risk decisions. | 5 templates | — | [sicario-spec](https://github.com/dfirs1car1o/sicario-spec) |
| Spec2Cloud | Spec-driven workflow tuned for shipping to Azure: spec → plan → tasks → implement → deploy | 5 templates, 8 commands | — | [spec2cloud](https://github.com/Azure-Samples/Spec2Cloud) |
| Table of Contents Navigation | Adds a navigable Table of Contents to generated spec.md, plan.md, and tasks.md documents | 3 templates, 3 commands | — | [spec-kit-preset-toc-navigation](https://github.com/Quratulain-bilal/spec-kit-preset-toc-navigation) |
| Test-First Governance | Governs TDD with coverage-complete BDD/ATDD Gherkin scenarios, explicit suite ownership, professional test reports, traceability, and risk-based quality gates. | 10 templates, 8 commands | — | [spec-kit-preset-test-first-governance](https://github.com/ka-zo/spec-kit-preset-test-first-governance) |
| VS Code Ask Questions | Enhances the clarify command to use `vscode/askQuestions` for batched interactive questioning. | 1 command | — | [spec-kit-presets](https://github.com/fdcastel/spec-kit-presets) |
2. Run `/speckit.plan` to define the implementation approach.
3. Run `/speckit.tasks` to derive the work breakdown.
4. Run `/speckit.implement` and review the resulting code and artifact diffs.
5. Run `/speckit.converge` to verify completeness and generate tasks for remaining gaps. If tasks are appended, repeat `/speckit.implement` and `/speckit.converge` until the feature is fully complete.
The previous feature directory remains intact for audit, comparison, or
explaining how the project reached its current state. Use clear feature names or
@@ -50,6 +51,7 @@ spec:
5. Run `/speckit.analyze` before implementation resumes to catch gaps between
the spec, plan, and tasks.
6. Run `/speckit.implement`, then review the code and artifact diffs together.
7. Run `/speckit.converge` to assess completion and append any remaining work to `tasks.md`. If tasks are appended, repeat `/speckit.implement` and `/speckit.converge` until the feature is fully complete.
Preserve important implementation rationale before replacing derived artifacts.
If a plan or task list contains decisions that still matter, carry them forward
@@ -31,7 +31,7 @@ Define what to build before building it. Rich templates, quality checklists, and
### Use any coding agent
<span class="pillar-stat">30+ integrations</span> — Copilot, Gemini, Codex, Windsurf, Zed, Claude, Forge, Kiro, and more. Switch freely between agents with a single command. No lock-in.
<span class="pillar-stat">30+ integrations</span> — Copilot, Gemini, Codex, Kilo Code, Zed, Claude, Forge, Kiro, and more. Switch freely between agents with a single command. No lock-in.
Run `specify init` with your agent of choice and Spec Kit sets up the right command files, context rules, and directory structures automatically. If your agent isn't listed, the `generic` integration is an escape hatch for any tool.
- [Git](https://git-scm.com/downloads) _(optional — required only when the git extension is enabled)_
@@ -15,7 +15,7 @@
### Persistent Installation (Recommended)
Install once and use everywhere. Replace `vX.Y.Z` with a tag from [Releases](https://github.com/github/spec-kit/releases):
Install once and use everywhere. Replace `vX.Y.Z` with a release tag from [Releases](https://github.com/github/spec-kit/releases) — keep the leading `v` (for example, `v0.12.11`, not `0.12.11`):
> [!NOTE]
> The command below requires **[uv](https://docs.astral.sh/uv/)**. If you see `command not found: uv`, [install uv first](./install/uv.md).
@@ -13,10 +13,10 @@ This guide will help you get started with Spec-Driven Development using Spec Kit
After installing Spec Kit and defining your project constitution, quick experiments can use the lean feature path: `/speckit.specify` -> `/speckit.plan` -> `/speckit.tasks` -> `/speckit.implement`. For production features or any work with meaningful ambiguity, treat `/speckit.clarify`, `/speckit.checklist`, and `/speckit.analyze` as regular quality gates:
Use `/speckit.clarify` to reduce requirement ambiguity before planning, `/speckit.checklist`to validate requirements quality before planning, and `/speckit.analyze` to check spec/plan/task consistency before implementation starts. You can repeat `/speckit.analyze` after implementation as an extra review, but keep the first analysis before `/speckit.implement` so gaps are caught while the plan and tasks can still be adjusted.
Use `/speckit.clarify` to reduce requirement ambiguity before planning, `/speckit.checklist`(after `/speckit.plan`) to generate quality checklists that validate requirements completeness, clarity, and consistency, and `/speckit.analyze` to check spec/plan/task consistency before implementation starts. You can repeat `/speckit.analyze` after implementation as an extra review, but keep the first analysis before `/speckit.implement` so gaps are caught while the plan and tasks can still be adjusted. Finally, run `/speckit.converge` after implementation to verify all planned work is complete and generate tasks for any remaining gaps. If `/speckit.converge` appends new tasks, run `/speckit.implement` again (and converge again) until it reports that the feature has converged.
/speckit.clarify Focus on security and performance requirements.
```
Then validate the requirements with `/speckit.checklist` before creating the technical plan:
```bash
/speckit.checklist
```
### Step 5: Create a Technical Implementation Plan
**In the chat**, use the `/speckit.plan` slash command to provide your tech stack and architecture choices.
@@ -89,6 +83,12 @@ Then validate the requirements with `/speckit.checklist` before creating the tec
/speckit.plan The application uses Vite with minimal number of libraries. Use vanilla HTML, CSS, and JavaScript as much as possible. Images are not uploaded anywhere and metadata is stored in a local SQLite database.
```
Then generate quality checklists with `/speckit.checklist` once the plan exists:
```bash
/speckit.checklist
```
### Step 6: Break Down, Analyze, and Implement
**In the chat**, use the `/speckit.tasks` slash command to create an actionable task list.
@@ -150,15 +150,7 @@ You can continue to refine the spec with more details using `/speckit.clarify`:
/speckit.clarify When you first launch Taskify, it's going to give you a list of the five users to pick from. There will be no password required. When you click on a user, you go into the main view, which displays the list of projects. When you click on a project, you open the Kanban board for that project. You're going to see the columns. You'll be able to drag and drop cards back and forth between different columns. You will see any cards that are assigned to you, the currently logged in user, in a different color from all the other ones, so you can quickly see yours. You can edit any comments that you make, but you can't edit comments that other people made. You can delete any comments that you made, but you can't delete comments anybody else made.
```
### Step 4: Validate the Spec
Validate the specification checklist using the `/speckit.checklist` command:
```bash
/speckit.checklist
```
### Step 5: Generate Technical Plan with `/speckit.plan`
### Step 4: Generate Technical Plan with `/speckit.plan`
Be specific about your tech stack and technical requirements:
@@ -166,6 +158,14 @@ Be specific about your tech stack and technical requirements:
/speckit.plan We are going to generate this using .NET Aspire, using Postgres as the database. The frontend should use Blazor server with drag-and-drop task boards, real-time updates. There should be a REST API created with a projects API, tasks API, and a notifications API.
```
### Step 5: Validate the Spec
Generate quality checklists to validate the specification using the `/speckit.checklist` command:
```bash
/speckit.checklist
```
### Step 6: Define Tasks
Generate an actionable task list using the `/speckit.tasks` command:
@@ -188,6 +188,14 @@ Finally, implement the solution:
/speckit.implement
```
### Step 8: Converge
Run the `/speckit.converge` command after implementation to assess the current codebase against the feature's artifacts and append any remaining unbuilt work as new tasks to `tasks.md`. If the command appends new tasks, run `/speckit.implement` again to complete them, and repeat the converge step until the feature is fully complete.
```bash
/speckit.converge
```
> [!TIP]
> **Phased Implementation**: For large projects like Taskify, consider implementing in phases (e.g., Phase 1: Basic project/task structure, Phase 2: Kanban functionality, Phase 3: Comments and assignments). This prevents context saturation and allows for validation at each stage.
| `--integration` | Override the integration used when refreshing components; applied only when the project's active integration can't be determined |
| `--offline` | Do not access the network |
Re-resolves a bundle and **refreshes** its components through each primitive's update path, bringing already-installed components up to the bundle's newly pinned versions while preserving primitive-level overrides (such as preset priority). Provide a bundle id, or use `--all` to update everything installed.
@@ -119,6 +120,12 @@ specify bundle build
Produces a single versioned, distributable `.zip` artifact from a bundle directory. The artifact embeds the manifest and can be installed directly with `specify bundle install <artifact.zip>`.
## Publish a Bundle
Bundle authors validate and package bundles locally, then host the generated artifact and catalog metadata where users can access it. A bundle catalog entry points at the bundle artifact, but the components declared inside `bundle.yml` still resolve through bundled components, installed components, or active extension, preset, workflow, and step catalogs.
If your bundle references components from non-default catalogs, document those catalog URLs and test the install path from a clean project with those catalogs added. Community bundle submissions should include that dependency-resolution evidence in the [Bundle Submission](https://github.com/github/spec-kit/issues/new?template=bundle_submission.yml) issue.
## Manage Catalog Sources
Bundles are discovered through a priority-ordered stack of catalog sources (project, user, and built-in scopes).
| `SPECIFY_INIT_DIR` | Target a member project from outside its directory (e.g. a monorepo root) without `cd`, for non-interactive / CI use. Set it to the **project root** — the directory *containing*`.specify/` (relative paths resolve against the current directory). The path must exist and contain `.specify/`, otherwise the command errors and does **not** fall back to the current directory. Resolved once in the core root helper (`get_repo_root` in Bash, `Get-RepoRoot` in PowerShell), so it is honored by the core feature scripts (`/speckit.plan`, `/speckit.tasks`, …) and the Git extension's feature-branch creation, which inherit it. When unset, the project is detected by searching upward from the current directory as before. |
| `SPECIFY_INIT_DIR` | Target a member project from outside its directory (e.g. a monorepo root) without `cd`, for non-interactive / CI use. Set it to the **project root** — the directory *containing*`.specify/` (relative paths resolve against the current directory). The path must exist and contain `.specify/`, otherwise the command errors and does **not** fall back to the current directory. Resolved once in the core root helper (`get_repo_root` in Bash, `Get-RepoRoot` in PowerShell), so it is honored by the core feature scripts (`/speckit.plan`, `/speckit.tasks`, …) and the Git extension's feature-branch creation, which inherit it. The `specify` CLI applies the **same** validation rules to every project-scoped subcommand (`specify integration …`, `specify extension …`, `specify workflow …`, `specify preset …`, and the rest that operate on a `.specify/` project), so those can target a member project too. When unset, Bash/PowerShell helpers keep their existing upward search; the `specify` CLI keeps its project-scoped resolver cwd-only unless a command explicitly defines broader detection (for example, bundle commands). |
| `SPECIFY_FEATURE_DIRECTORY` | Override the active feature directory *within* the resolved project (takes precedence over `.specify/feature.json`). Relative paths resolve under the project root. Combine with `SPECIFY_INIT_DIR` to pick both the project and the feature non-interactively. |
| `SPECIFY_FEATURE` | Override feature detection for non-Git repositories. Set to the feature directory name (e.g., `001-photo-albums`) to work on a specific feature when not using Git branches. Must be set in the context of the agent prior to using `/speckit.plan` or follow-up commands. |
> **Two resolution axes.** `SPECIFY_INIT_DIR` selects the **project** (which directory contains `.specify/`); `SPECIFY_FEATURE_DIRECTORY` / `.specify/feature.json` select the **feature** within that project. They are independent — project first, then feature.
> **Symlinked project roots.** `SPECIFY_INIT_DIR` relocates *where* the project is, not *how* a command treats symlinks: each command keeps its existing cwd-path stance. Commands that traverse and write project files through broad input paths (`bundle`, `workflow run <file>`) refuse a symlinked `.specify/` to preserve write confinement. Other project-scoped commands keep their existing behavior when `SPECIFY_INIT_DIR` points at a project root, which may include following a symlinked `.specify/`.
Installs an extension from the catalog, a URL, or a local directory. Extension commands are automatically registered with the currently installed AI coding agent integration.
| [Codex CLI](https://github.com/openai/codex) | `codex` | Skills-based integration; installs skills into `.agents/skills` and invokes them as `$speckit-<command>` |
| [Devin for Terminal](https://cli.devin.ai/docs) | `devin` | Skills-based integration; installs skills into `.devin/skills/` and invokes them as `/speckit-<command>` |
| [Firebender](https://firebender.com/) | `firebender` | IDE-based agent for Android Studio / IntelliJ |
| [Goose](https://block.github.io/goose/) | `goose` | Uses YAML recipe format in `.goose/recipes/` |
| [GitHub Copilot](https://code.visualstudio.com/) | `copilot` | Defaults to legacy markdown mode: `.agent.md` command files under `.github/agents/`, companion `.prompt.md` files under `.github/prompts/`, and a `.vscode/settings.json` merge. Pass `--integration-options="--skills"` to scaffold skills as `speckit-<command>/SKILL.md` under `.github/skills/` instead. Legacy markdown mode is deprecated and will stop being the default in a future release. |
| [Goose](https://goose-docs.ai/) | `goose` | Uses YAML recipe format in `.goose/recipes/` |
| [Kimi Code](https://code.kimi.com/) | `kimi` | Skills-based integration; installs into `.kimi-code/skills/`. `--migrate-legacy` moves old `.kimi/skills/` installs to the new paths |
| [Kiro CLI](https://kiro.dev/docs/cli/) | `kiro-cli` | Kiro CLI does not substitute `$ARGUMENTS` in file-based prompts, so Spec Kit ships a prose fallback at render time (see [Manage prompts](https://kiro.dev/docs/cli/chat/manage-prompts/) and issue [#1926](https://github.com/github/spec-kit/issues/1926)). Alias: `--integration kiro` |
| [Pi Coding Agent](https://pi.dev) | `pi` | Pi doesn't have MCP support out of the box, so `taskstoissues` won't work as intended. MCP support can be added via [extensions](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent#extensions) |
| [ZCode](https://zcode.z.ai/) | `zcode` | Skills-based integration; installs skills into `.zcode/skills/` and invokes them as `$speckit-<command>` |
| [Zed](https://zed.dev/) | `zed` | Skills-based integration; installs skills into `.agents/skills` and invokes them as `/speckit-<command>` |
| Generic | `generic` | Bring your own agent — use `--integration generic --integration-options="--commands-dir <path>"` for AI coding agents not listed above |
@@ -53,6 +51,27 @@ Shows all available integrations, which one is currently installed, and whether
When multiple integrations are installed, the list marks the default integration separately from the other installed integrations.
The list also shows whether each built-in integration is declared multi-install safe.
## Search Available Integrations
```bash
specify integration search [query]
```
| Option | Description |
| ---------- | ------------------ |
| `--tag` | Filter by tag |
| `--author` | Filter by author |
Searches the active catalog stack for integrations matching the query. Without a query, lists all available integrations. Must be run inside a Spec Kit project.
## Integration Info
```bash
specify integration info <integration_id>
```
Shows catalog details for a single integration, including its description, author, license, tags, source catalog, repository (when available), and whether it is currently active. Must be run inside a Spec Kit project.
| `--force` | Force removal of modified files during uninstall; when the target is already installed, overwrite managed shared templates while changing the default |
| `--refresh-shared-infra` | Also overwrite shared infrastructure files even if you customized them (otherwise customizations are preserved) |
| `--integration-options` | Options for the target integration when it is not already installed |
If the target integration is not already installed, equivalent to running `uninstall` followed by `install` in a single step. In this mode, `--force` controls whether modified files from the removed integration are deleted. If the target integration is already installed, `switch` only changes the default integration, like `use`; in this mode, `--force` controls whether managed shared templates are overwritten while the default changes. `--integration-options` is rejected for already-installed targets because changing integration options requires reinstalling managed files; run `upgrade <key> --integration-options ...` first, then `use <key>`.
@@ -150,6 +170,47 @@ is `null` when no installed integration set can be evaluated, such as when the
integration state is missing, unreadable, lacks a valid recorded integration
list, or records no installed integrations.
## Catalog Management
Integration catalogs control where the discovery commands (`search` and `info`) look for integrations. Catalogs are checked in priority order.
### List Catalogs
```bash
specify integration catalog list
```
Shows the active catalog sources. Project-level sources (when configured) are removable by index; otherwise the active sources are shown as non-removable.
| `--name <name>` | Optional name for the catalog |
Adds a custom catalog URL to the project's `.specify/integration-catalogs.yml`. The URL must use HTTPS (except `http://localhost`, `http://127.0.0.1`, or `http://[::1]` for local testing).
### Remove a Catalog
```bash
specify integration catalog remove <index>
```
Removes a project catalog source by its 0-based index in `catalog list`.
### Catalog Resolution Order
Catalogs are resolved in this order (first match wins):
1.**Environment variable** — `SPECKIT_INTEGRATION_CATALOG_URL` overrides all catalogs
| `kimi` | `--migrate-legacy` | Migrate legacy dotted skill directories to hyphenated format |
| `kimi` | `--migrate-legacy` | Migrate legacy `.kimi/skills/` installs to `.kimi-code/skills/` (including dotted→hyphenated skill naming, e.g. `speckit.xxx` → `speckit-xxx`) |
| `copilot` | `--skills` | Scaffold commands as agent skills (`speckit-<command>/SKILL.md` under `.github/skills/`, invoked as `/speckit-<command>`) instead of the default legacy markdown mode (`.github/agents/*.agent.md` plus `.github/prompts/*.prompt.md` and a `.vscode/settings.json` merge). Without this flag, install warns that legacy markdown mode is deprecated. |
Creates a minimal built-in integration package and a matching test skeleton in the Spec Kit repository, then prints the next steps for wiring it up. Run this command from the Spec Kit repository root. The `<key>` must be lowercase kebab-case (for example, `my-agent`).
Integrations that share a context file or command directory with another integration, require dynamic install paths such as `--commands-dir`, or merge shared tool settings are not declared safe by default. They can still be installed alongside another integration with `--force`.
@@ -212,7 +284,7 @@ Run `specify integration list` to see all available integrations with their keys
### Do I need the AI coding agent installed to use an integration?
CLI-based integrations (like Claude Code, Gemini CLI) require the tool to be installed. IDE-based integrations (like Windsurf, Cursor) work through the IDE itself. Some agents like GitHub Copilot support both IDE and CLI usage. `specify integration list` shows which type each integration is.
CLI-based integrations (like Claude Code, Gemini CLI) require the tool to be installed. IDE-based integrations (like Cursor) work through the IDE itself. Some agents like GitHub Copilot support both IDE and CLI usage. `specify integration list` shows which type each integration is.
Presets can provide command files, template files (like `plan-template.md`), and script files. These are resolved at runtime using a **replace** strategy — the first match in the priority stack wins and is used entirely. Each file is looked up independently, so different files can come from different layers.
Presets can provide command files, template files (like `plan-template.md`), and script files. Each file name is evaluated independently against the priority stack, so different files can come from different layers.
> **Note:** Additional composition strategies (`append`, `prepend`, `wrap`) are planned for a future release.
Templates and scripts are looked up from the stack when Spec Kit needs them. Commands use the same stack for replacement and composition, but are materialized into detected agent directories instead of being re-resolved by agents. During preset install, Spec Kit registers command files for the preset being installed; post-install and post-removal reconciliation then recomputes and writes the effective command content for affected command names based on the active stack. Agents do not re-resolve the stack each time they run a command.
By default, files use a **replace** strategy: the first match in the priority stack wins and is used entirely. Templates and commands can also use composition strategies: **prepend** places preset content before lower-priority content, **append** places it after lower-priority content, and **wrap** replaces `{CORE_TEMPLATE}` with lower-priority content. Scripts support **replace** and **wrap**; script wrappers use `$CORE_SCRIPT` as the placeholder.
The resolution stack, from highest to lowest precedence:
@@ -148,8 +150,6 @@ The resolution stack, from highest to lowest precedence:
3.**Installed extensions** — sorted by priority
4.**Spec Kit core** — `.specify/templates/`
Commands are registered at install time (not resolved through the stack at runtime).
### Resolution Stack
```mermaid
@@ -215,7 +215,7 @@ Run `specify preset resolve <name>` to trace the resolution stack and see which
### What's the difference between disabling and removing a preset?
**Disabling** (`specify preset disable`) keeps the preset installed but excludes its files from the resolution stack. Commands the preset registered remain available in your AI coding agent. This is useful for temporarily testing behavior without a preset, or comparing output with and without it. Re-enable anytime with `specify preset enable`.
**Disabling** (`specify preset disable`) keeps the preset installed but excludes it from future template and script resolution. Previously registered commands remain available in your AI coding agent until preset removal, so use removal when you need command changes to stop taking effect. Disabling is useful for temporarily testing template/script behavior without a preset, or comparing template/script output with and without it. Re-enable anytime with `specify preset enable`.
**Removing** (`specify preset remove`) fully uninstalls the preset — deletes its files, unregisters its commands from your AI coding agent, and removes it from the registry.
| `--dev` | Install from a local workflow YAML file or directory |
| `--from <url>` | Install from a custom URL (`<source>` names the expected workflow ID) |
Installs a workflow from the catalog, a URL (HTTPS required), or a local file path.
## Update Workflows
```bash
specify workflow update [workflow_id]
```
Updates one installed catalog workflow — or all of them when no ID is given — to the latest catalog version. Prompts for confirmation and keeps the installed copy if a download or validation fails.
## Enable or Disable a Workflow
```bash
specify workflow enable <workflow_id>
specify workflow disable <workflow_id>
```
Disabled workflows stay installed and listed (marked `[disabled]`) but refuse to run until re-enabled.
## Remove a Workflow
```bash
@@ -102,9 +124,10 @@ Removes an installed workflow from the project.
specify workflow search [query]
```
| Option | Description |
| ------- | --------------- |
| `--tag` | Filter by tag |
| Option | Description |
| ---------- | ----------------- |
| `--tag` | Filter by tag |
| `--author` | Filter by author |
Searches all active catalogs for workflows matching the query.
@@ -262,6 +285,7 @@ specify workflow run speckit -i spec="Build a kanban board with drag-and-drop ta
| `prompt` | Send an arbitrary prompt to the AI coding agent |
| `shell` | Execute a shell command and capture output |
| `init` | Bootstrap a project (like `specify init`) |
| `gate` | Pause for human approval before continuing |
| `if` | Conditional branching (then/else) |
| `switch` | Multi-branch dispatch on an expression |
@@ -270,6 +294,8 @@ specify workflow run speckit -i spec="Build a kanban board with drag-and-drop ta
| `fan-out` | Dispatch a step for each item in a list |
| `fan-in` | Aggregate results from a fan-out step |
> **Security note:** a `shell` step runs a local command with **your** privileges. There is no capability sandbox — `requires` is an advisory pre-condition block (spec-kit version, integrations), not a runtime gate, so it does **not** restrict what a step can do. In particular there is no `requires.permissions` capability gate: it is rejected by validation precisely because it would imply a sandbox that does not exist. Review any catalog or downloaded workflow before running it, and use a `gate` step to require explicit approval before sensitive or destructive shell commands.
## Expressions
Steps can reference inputs and previous step outputs using `{{ expression }}` syntax:
### Scenario 3: "I see duplicate slash commands in my IDE"
This happens with IDE-based agents (Kilo Code, Windsurf, Roo Code, etc.).
This happens with IDE-based agents (Kilo Code, Cline, etc.).
```bash
# Find the agent folder (example: .kilocode/rules/)
cd .kilocode/rules/
# Find the agent folder (example: .kilocode/workflows/)
cd .kilocode/workflows/
# List all files
ls -la
@@ -308,6 +308,7 @@ Alternatively, run the `/speckit.specify` command which creates `.specify/featur
ls -la .gemini/commands/ # Gemini
ls -la .cursor/skills/ # Cursor
ls -la .pi/prompts/ # Pi Coding Agent
ls -la .omp/commands/ # Oh My Pi
```
3. **Check agent-specific setup:**
@@ -427,7 +428,7 @@ The `specify` CLI tool is used for:
- **Upgrades:** `specify init --here --force` to update templates and commands
- **Diagnostics:** `specify check` to verify tool installation
Once you've run `specify init`, the slash commands (like `/speckit.specify`, `/speckit.plan`, etc.) are **permanently installed** in your project's agent folder (`.claude/`, `.github/prompts/`, `.pi/prompts/`, etc.). Your AI coding agent reads these command files directly—no need to run `specify` again.
Once you've run `specify init`, the slash commands (like `/speckit.specify`, `/speckit.plan`, etc.) are **permanently installed** in your project's agent folder (`.claude/`, `.github/prompts/`, `.pi/prompts/`, `.omp/commands/`, etc.). Your AI coding agent reads these command files directly—no need to run `specify` again.
**If your agent isn't recognizing slash commands:**
@@ -442,6 +443,9 @@ Once you've run `specify init`, the slash commands (like `/speckit.specify`, `/s
# For Pi
ls -la .pi/prompts/
# For Oh My Pi
ls -la .omp/commands/
```
2. **Restart your IDE/editor completely** (not just reload window)
@@ -6,15 +6,17 @@ It owns the lifecycle of the managed section delimited by the configurable start
## Why an extension?
Not every Spec Kit user wants Spec Kit to write into the coding agent's context file. Extracting this behavior into a dedicated extension lets users:
Not every Spec Kit user wants Spec Kit to write into the coding agent's context file. Keeping this behavior in a dedicated, **opt-in** extension lets users:
- **Opt out** entirely with `specify extension disable agent-context` — Spec Kit will then never create or modify the agent context file.
- **Customize the markers** by editing `.specify/extensions/agent-context/agent-context-config.yml` — both the Python layer and the bundled scripts honor the same `context_markers` value.
- **Choose whether to install it at all** — `specify init` does not install it. Add it explicitly when you want Spec Kit to manage the agentcontext file; if it is absent or disabled, Spec Kit never creates or modifies that file.
- **Customize the markers** by editing `.specify/extensions/agent-context/agent-context-config.yml` — the bundled scripts honor the `context_markers` value.
- **Synchronize multiple agent anchors** by setting `context_files` when a project intentionally uses more than one coding agent context file, such as `AGENTS.md` and `CLAUDE.md`.
- **Refresh on demand** with `/speckit.agent-context.update`, or automatically through the hooks declared in `extension.yml` (`after_specify`, `after_plan`).
- **Refresh on demand** by running the`speckit.agent-context.update` command in your agent, or automatically through the hooks declared in `extension.yml` (`after_specify`, `after_plan`). Invoke it using your agent's slash-command separator — `/speckit.agent-context.update` for dot-separator agents or `/speckit-agent-context-update` for hyphen-separator agents (e.g. Forge, Cline).
## Commands
The command ID below is canonical. When invoking it as a slash command, use your agent's separator: `/speckit.agent-context.update` for dot-separator agents or `/speckit-agent-context-update` for hyphen-separator agents (e.g. Forge, Cline).
| Command | Description |
|---------|-------------|
| `speckit.agent-context.update` | Refresh the managed section in the agent context file with the current plan path. |
@@ -40,7 +42,7 @@ context_markers:
end:"<!-- SPECKIT END -->"
```
-`context_file` — the project-relative path to the coding agent context file, written by `specify init` and `specify integration install`.
-`context_file` — the project-relative path to the coding agent context file. When empty, the bundled update scripts self-seed it by looking up the active integration's key in this extension's own `agent-context-defaults.json` map. The Specify CLI is never consulted.
-`context_files` — optional project-relative paths to multiple coding agent context files. When non-empty, the list takes precedence over `context_file`. Absolute paths, backslash separators, and `..` path segments are rejected.
-`context_markers.start` / `.end` — the delimiters around the managed section. Edit these to use custom markers.
@@ -62,5 +64,4 @@ pip install pyyaml
specify extension disable agent-context
```
When disabled, Spec Kit skips context file creation, updates, and removal (the gates are inside `upsert_context_section()` and `remove_context_section()`).
Disabled projects also ignore stale `context_files` values during command rendering so disabling the extension remains a complete opt-out.
When disabled (or never installed), Spec Kit performs no agent context file creation, updates, or removal — the extension's bundled scripts are the only code that ever touches the managed section. The Specify CLI carries no agent-context state at all: it never reads this config, never resolves a context file, and the `__CONTEXT_FILE__` placeholder (if present in any template) is left untouched. All context-file knowledge — including the per-agent default mapping in `agent-context-defaults.json` — lives entirely within this extension, so disabling it is a complete opt-out.
"_comment":"Default coding agent context file per integration, owned by the agent-context extension. Used to self-seed agent-context-config.yml when it declares no context_file/context_files. Keyed by the Spec Kit integration key recorded in .specify/init-options.json. This mapping is independent of the Specify CLI by design.",
@@ -15,7 +15,7 @@ The script reads the agent-context extension config at
-`context_files` — optional project-relative paths for multiple coding agent context files. When non-empty, the script updates each listed file and the list takes precedence over `context_file`.
-`context_markers.start` / `.end` — the delimiters surrounding the managed section. Defaults to `<!-- SPECKIT START -->` and `<!-- SPECKIT END -->` when the field is missing.
It then creates, replaces, or appends the managed block so that the section points at the most recent plan path when one can be discovered (`specs/<feature>/plan.md`).
It then creates, replaces, or appends the managed block so that the section points at the most recent plan path when one can be discovered (any `plan.md` under `specs/`, including nested scoped layouts such as `specs/<scope>/<feature>/plan.md`).
If `context_files` and `context_file` are empty, the command reports nothing to do and exits successfully. Context file paths must stay project-relative; absolute paths, Windows drive paths, backslash separators, and `..` path segments are rejected.
@@ -24,4 +24,4 @@ If `context_files` and `context_file` are empty, the command reports nothing to
When `plan_path` is omitted, the script auto-detects the most recently modified `specs/*/plan.md`.
When `plan_path` is omitted, the script auto-detects the most recently modified `specs/**/plan.md` (searched recursively, so nested scoped layouts are discovered).
Write-Warning("agent-context: no default context file is known for integration '{0}'; set 'context_file' in the extension config to choose one."-f$integrationKey)
}
}else{
Write-Warning("agent-context: unable to read {0}; cannot self-seed the context file. Set 'context_file' in the extension config."-f$defaultsPath)
}
}
}catch{
# Non-fatal: fall through to the nothing-to-do guard below.
}
}
}
if($ContextFiles.Count-eq0){
Write-Warning'agent-context: context_files/context_file not set in extension config; nothing to do.'
exit0
@@ -280,21 +383,71 @@ if ($cm) {
}
if(-not$PlanPath){
# Discover plan.md exactly one level deep (specs/<feature>/plan.md),
# matching the bash glob specs/*/plan.md. Wrap in try/catch so access errors under
# $ErrorActionPreference = 'Stop' don't abort the script.
"description":"Live spec-driven progress for SpecKit Companion — lifecycle capture, status, resume, and a turbo pipeline profile.",
"description":"Live spec-driven progress for SpecKit Companion — lifecycle capture, status, resume, and composable commands you can customize with hooks and recipes.",
"description":"Canonical-Driven Development enforcement. Validates, scores, and traces project documentation with automated checks, AI-driven workflows, and spec-kit hooks. One pinned runtime dependency; pure Node.js otherwise.",
"description":"Doc-integrity engine with MCP server, SARIF output, and zero-LLM core. Validates, scores, and traces documentation against code — 24 validators, stable finding codes, spec-kit hooks. Pure Node.js.",
"description":"Author, lint, and convert requirements using EARS (Easy Approach to Requirements Syntax) - the five industry-standard sentence patterns for unambiguous, testable requirements.",
"description":"Deterministic quality enforcement for Spec Kit projects at three boundaries — agent hooks, git pre-commit, CI — with one policy file and provable enforcement (attestations, canaries, verified parity).",
"description":"Deterministic behavioral drift oracle. Extracts acceptance criteria, generates fuzz test vectors (seed=42), compares golden Python implementations against real code in any language. CI/CD gatekeeper with warn/strict modes.",
"description":"An idempotent, drift-aware, fail-closed reconcile engine that mirrors spec-kit specs into Jira (Epic per repo, Story per spec, Subtask per phase).",
"description":"Recalls prior specs and decisions from configurable memory tools (e.g. memsearch) before SDLC stages, so planning and specification start from what the project already knows.",
"description":"Detect side effects that tests can't catch after implementation — delta-anchored analysis across 9 domain-agnostic categories with fix-induced side effect detection",
"description":"Detect side effects that tests can't catch after implementation — surface hidden ripple effects across 9 analysis categories",
"description":"Capture a durable spec roadmap after the constitution, then review specs against it before and after implementation so spec-specific decisions, outcomes, and constraints are never lost.",
This extension provides Git operations as an optional, self-contained module. It manages:
- **Repository initialization** with configurable commit messages
- **Feature branch creation** with sequential (`001-feature-name`) or timestamp (`20260319-143022-feature-name`) numbering
- **Feature branch creation** with sequential (`001-feature-name`) or timestamp (`20260319-143022-feature-name`) numbering and optional templates for branch namespaces
- **Branch validation** to ensure branches follow naming conventions
- **Git remote detection** for GitHub integration (e.g., issue creation)
- **Auto-commit** after core commands (configurable per-command with custom messages)
@@ -53,6 +53,16 @@ Configuration is stored in `.specify/extensions/git/git-config.yml`:
# Branch numbering strategy: "sequential" or "timestamp"
branch_numbering:sequential
# Optional branch name template. Leave empty for the default "{number}-{slug}".
# Supported tokens: {author}, {app}, {number}, {slug}; {slug} must not appear
# before {number}, and the final path segment must start with {number}-.
# Example for monorepos: "{author}/{app}/{number}-{slug}"
branch_template:""
# Optional shorthand namespace. Leave empty to use branch_template/default behavior.
# Example: "features/{app}" expands to "features/{app}/{number}-{slug}"
branch_prefix:""
# Custom commit message for git init
init_commit_message:"[Spec Kit] Initial commit"
@@ -65,6 +75,10 @@ auto_commit:
message:"[Spec Kit] Add specification"
```
`{author}` is derived from Git config and sanitized for branch names. `{app}` is derived from the Spec Kit init directory name. Custom templates must not put `{slug}` before `{number}`, and must put `{number}-` at the start of the final path segment so generated names remain valid feature branches. For a monorepo project at `apps/web/.specify/`, a template such as `{author}/{app}/{number}-{slug}` produces branches like `jdoe/web/008-guided-tour`.
For simple namespace-only customization, `branch_prefix` is also accepted as a shorthand and expands to `<branch_prefix>/{number}-{slug}`.
@@ -19,7 +19,7 @@ You **MUST** consider the user input before proceeding (if not empty).
If the user explicitly provided `GIT_BRANCH_NAME` (e.g., via environment variable, argument, or in their request), pass it through to the script by setting the `GIT_BRANCH_NAME` environment variable before invoking the script. When `GIT_BRANCH_NAME` is set:
- The script uses the exact value as the branch name, bypassing all prefix/suffix generation
-`--short-name`, `--number`, and `--timestamp` flags are ignored
-`FEATURE_NUM` is extracted from the name if it starts with a numeric prefix, otherwise set to the full branch name
-`FEATURE_NUM` is extracted when the final path segment starts with a numeric or timestamp feature marker (for example `042-name`, `feat/042-name`, or `jdoe/app/042-name`), otherwise set to the full branch name
## Prerequisites
@@ -35,6 +35,19 @@ Determine the branch numbering strategy by checking configuration in this order:
3. Check `.specify/init-options.json` for `branch_numbering` value (deprecated, backward compatibility — will be removed in a future release)
4. Default to `sequential` if none of the above exist
## Branch Name Template
Check `.specify/extensions/git/git-config.yml` for an optional `branch_template` value. If it is empty or missing, use the default branch shape `{number}-{slug}`. If it is set, `{slug}` must not appear before `{number}`, its final path segment must start with `{number}-`, and the script expands these tokens:
-`{author}`: sanitized Git config author (`user.name`, falling back to the email local part)
-`{app}`: sanitized Spec Kit init directory name
-`{number}`: sequential number or timestamp
-`{slug}`: generated short branch slug
For monorepos, a template such as `{author}/{app}/{number}-{slug}` creates names like `jdoe/web/008-guided-tour` while preserving per-project feature numbering.
The script also accepts `branch_prefix` as a shorthand for simple namespaces; it expands to `<branch_prefix>/{number}-{slug}`.
## Execution
Generate a concise short name (2-4 words) for the branch:
@@ -54,6 +67,7 @@ Run the appropriate script based on your platform:
- Always include the JSON flag (`--json` for Bash, `-Json` for PowerShell) so the output can be parsed reliably
- You must only ever run this script once per feature
- The JSON output will contain `BRANCH_NAME` and `FEATURE_NUM`
- Do not manually expand `branch_template`; the script reads the git extension config and applies it consistently
## Graceful Degradation
@@ -64,5 +78,5 @@ If Git is not installed or the current directory is not a Git repository:
## Output
The script outputs JSON with:
-`BRANCH_NAME`: The branch name (e.g., `003-user-auth` or`20260319-143022-user-auth`)
-`BRANCH_NAME`: The branch name (e.g., `003-user-auth`,`20260319-143022-user-auth`, or `jdoe/web/003-user-auth`)
-`FEATURE_NUM`: The numeric or timestamp prefix used
throw"GIT_BRANCH_NAME must be 244 bytes or fewer in UTF-8. Provided value is $branchNameUtf8ByteCount bytes; please supply a shorter override branch name."
}
# Extract FEATURE_NUM from the branch name if it starts with a numeric prefix
# Check timestamp pattern first (YYYYMMDD-HHMMSS-) since it also matches the simpler ^\d+ pattern
[Console]::Error.WriteLine("ERROR: Not on a feature branch. Current branch: $raw")
[Console]::Error.WriteLine("Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name")
[Console]::Error.WriteLine("Feature branches should be named like: 001-feature-name, 1234-feature-name, 20260319-143022-feature-name, or <prefix>/001-feature-name")
This edition covers Spec Kit activity in June 2026 — a month of maturation and mainstream validation. Twenty-five releases shipped (v0.9.0 through v0.12.2), spanning four minor bumps and delivering two headline capabilities: the **`/speckit.converge` command**, which closes the loop between a spec and the code that implements it, and the new **`specify bundle` subsystem**, a role-based distribution layer that composes extensions, presets, workflows, and steps into a single installable unit. The workflow engine became programmable, the git extension went opt-in as the first real breaking change, and the ecosystem crossed **120+ community extensions**. Externally, June was the highest-volume press month on record — Microsoft's own Developer Blog published a first-party spec-driven development post, an enterprise reported 2–4× velocity gains, and 75 substantive articles appeared across 25+ languages. A summary is in the table below, followed by details.
| Twenty-five releases shipped (v0.9.0–v0.12.2) with key features: the `/speckit.converge` convergence loop, the `specify bundle` role-based packaging subsystem, a programmable workflow engine (step catalog, JSON output, `from_json`), the git extension becoming opt-in (`--no-git` removed), and six new agents (Cline, rovodev, Zed, Firebender, ZCode, omp). The repo grew from ~107k to **~116,500 stars**. [\[github.com\]](https://github.com/github/spec-kit/releases) | The community extension catalog grew from 105 to **124 entries**; presets reached **23**. Microsoft's Developer Blog published a first-party SDD post naming Spec Kit as the operationalizing toolkit. June was the highest-volume press month yet — **75 substantive articles** across 25+ languages. **245 contributors** now listed. | An enterprise (SNCF Connect & Tech) reported **2–4× velocity** from SDD. Analysts and comparisons increasingly name Spec Kit "the category anchor" and agent-neutral default. Competitors differentiate on brownfield and drift; balanced reviews continue to flag review-overload and ceremony for small tasks. |
***
> **Spec-Driven Development, Institutionalized.** If May was defined by milestone 100s, June was defined by validation from outside the project. Microsoft's own Developer Blog published a first-party post presenting spec-driven development and positioning Spec Kit as the toolkit that operationalizes it. An enterprise — SNCF Connect & Tech — went on the record with **2–4× velocity gains** from adopting SDD. A record **75 substantive articles** appeared in more than 25 languages, and the recurring verdict across independent comparisons was that Spec Kit is "the category anchor" and the agent-neutral default. Meanwhile the core matured from v0.9 to v0.12: the workflow engine became genuinely programmable, the first real breaking change shipped, and the new convergence loop and bundle subsystem gave the project answers to its two most-cited gaps — drift and distribution. None of this happens without the community — the contributors, extension and preset authors, bundle builders, and practitioners writing in a dozen languages. Thank you.
## Spec Kit Project Updates
### Releases Overview
**v0.9.0–v0.9.5** (June 1–5) opened the month with a minor bump and five patches. The headline was **native Cline integration** (#2508) and **rovodev** support (#2539), plus the long-running effort to extract agent-context updates into a bundled, opt-in **`agent-context` extension** (#2546, closing #2398). The CLI gained **`specify self upgrade`** (#2475) and a **`--force` flag for `extension add`** (#2530). The workflow engine picked up four capabilities: running YAML files **without a project** (#2825), accepting **updated inputs on resume** (#2815), **structured JSON output** across `run`/`resume`/`status` (#2814), and a **`continue_on_error` step field** for non-halting failures (#2663). Windows compatibility hardened with UTF-8 stdout/stderr (#2817), and cursor-agent headless dispatch now works end-to-end (#2631). [\[github.com\]](https://github.com/github/spec-kit/releases)
**v0.10.0–v0.10.4** (June 9–16) delivered the month's first real **breaking change**: the **git extension is now opt-in** and the long-deprecated `--no-git` flag was removed at v0.10.0 (#2873, closing #2168). A long-standing community ask landed as **per-event hook lists with priority ordering** (#2798, closing #2378), letting extensions cleanly compose multiple hooks on one event. Operators gained a **`specify integration status`** reporting command (#2674), and the extension schema picked up first-class **`category` and `effect` fields** (#2899) to natively express the `Candidate`/`Adjacent`/`Niche`/`Bridge` signals. Security-relevant fixes hardened **preset URL installs against unsafe redirects** (#2911) and preserved the Claude `SKILL.md``argument-hint` for extension commands (#2916). [\[github.com\]](https://github.com/github/spec-kit/releases)
**v0.11.0–v0.11.10** (June 16–29) was the largest release cluster of the month and centered on **workflows** and the new **convergence loop**. The **`/speckit.converge` command** shipped (#3001), and the **workflow step catalog** made workflow steps community-installable the way extensions and presets already are (#2394, closing #2216). A complementary **`init` workflow step** (#2838) lets a workflow bootstrap a project the way `specify init` does. Workflow execution became programmable: opt-in `output_format: json` exposes parsed shell stdout as `output.data` (#2963), and a new **`from_json` expression filter** (#2961) turns step outputs into typed values. The new **`bug-assess` agentic workflow** (#3023) automates bug triage from labeled issues, **Zed** joined the supported agents (#2780), and contributors gained an **integration scaffolder** (#2685). The **`specify bundle` command** made its debut here (#3070). Two Windows/PowerShell pain points closed — `specify init` no longer hangs on PowerShell 5.1 (#2938) and the 233-day-old worktree branch-numbering bug was fixed (#3054, closing #1066). [\[github.com\]](https://github.com/github/spec-kit/releases)
**v0.12.0–v0.12.2** (June 29–30) closed the month with a minor bump making the **`agent-context` extension a full opt-in** (#3097) and a run of workflow-engine hardening: `max_concurrency` is now honored in fan-out via a bounded thread pool (#3224), gate validation no longer crashes on non-string options (#3233), pipe-filter detection became quote-aware (#3232), and a fan-in `wait_for` that names an unknown step is now rejected at validation (#3225). Three agents were also rationalized — **Firebender** (Android Studio / IntelliJ, #3077, closing #1548), **ZCode** (Z.AI, #3063), and **omp** (#3107) joined earlier in the run, while **Windsurf** was absorbed into Cognition Devin (#3168) and **iflow** was retired as discontinued (#3166). [\[github.com\]](https://github.com/github/spec-kit/releases)
### The Convergence Loop: `/speckit.converge`
The most significant addition to the SDD workflow since the core commands themselves, **`/speckit.converge`** (#3001) adds a ninth step that runs *after*`/speckit.implement` and answers the single most-cited concern in every review of the project: *does the code actually match the spec?*
Converge reads `spec.md`, `plan.md`, and `tasks.md` as the **sole source of intent** — with the constitution as governing constraints — assesses the current state of the code, and appends any remaining unbuilt work as new, traceable tasks. It is deliberately **not** a diff or git tool: it evaluates the *present* state of the code relative to the feature's artifacts, with no branch comparison and no history. Findings are classified by **gap type** — `missing` (absent entirely), `partial` (present but incomplete), `contradicts` (conflicts with intent or a constitution MUST), or `unrequested` (work the spec never called for) — and graded by severity, with a constitution-MUST violation always the highest.
Its defining design choice is that it is **append-only and never rewrites**. Its only write is a new `## Phase N: Convergence` section at the bottom of `tasks.md`; it never modifies the spec or plan, never renumbers existing tasks, and never touches application code — completing the appended tasks remains the job of `/speckit.implement`. When the codebase already satisfies everything, it leaves `tasks.md` byte-for-byte unchanged and simply reports **"✅ Converged."** Each appended task carries a `source-ref` (e.g. `FR-003`, `SC-002`, `US1/AC2`, a plan decision, or a constitution article), preserving traceability from requirement to remediation.
The result is an **iterative convergence loop** — converge → implement → converge — that runs until no gaps remain. It also smooths migration from OpenSpec by giving Spec Kit a first-class verify-and-close-the-gap step (#2673), directly answering the drift-and-verification demand the community had been expressing through extensions like Architecture Guard, Spec Trace, and the various drift-control tools. The command is now documented in the quickstart and the evolving-specs guide. [\[github.com\]](https://github.com/github/spec-kit/blob/main/docs/quickstart.md)
### The Bundle Subsystem: `specify bundle`
June's second headline was the debut of **bundles** (#3070), a distribution and composition layer that sits above the existing primitives. Where extensions, presets, workflows, and steps are the building blocks, a **bundle is a curated, versioned, role-based stack** that declares everything a team or role needs and installs it in a single step. Crucially, a bundle adds *no new runtime behavior of its own* — it composes what already exists through each component's own machinery, so there is nothing new to learn at execution time.
A bundle is described by a **`bundle.yml` manifest**: metadata (`id`, `name`, `version`, `role`, `author`, `license`), a `requires` block (minimum `speckit_version`, tools, MCP servers), and a `provides` block listing the exact extensions, presets (with `priority` and composition `strategy`), steps, and workflows it installs — each pinned to a version. The first example bundles ship four roles: **developer, product-manager, business-analyst, and security-researcher**.
The subcommand surface is a full package-manager experience: `search` and `info` (which previews the **fully expanded component set** with pinned versions and a `verified`-vs-`community` trust indicator before you install), `install`, `update` (`--all`), `remove`, `list`, `init`, `validate`, `build` (produces a single versioned `.zip` artifact), `publish`, and `catalog` management (`list`/`add`/`remove` sources). Installs are **idempotent with full provenance tracking**, so a bundle can be cleanly removed or refreshed later; `remove` uninstalls only the components a bundle contributed, leaving anything another installed bundle still needs in place. If run in a directory that isn't yet a Spec Kit project, `install` and `init`**bootstrap one first**, so a fresh checkout reaches a working state in a single command. The only cross-bundle conflict point checked at install time is the active integration.
Bundles are discovered through the same priority-ordered catalog stack (project, user, and built-in scopes) as every other component, and by the end of the month they had become a **fourth community-submittable artifact type** alongside extensions, presets, and workflows, via a dedicated submission path (#3162). Bundles are the project's answer to the "how do I distribute a whole role setup?" question — the composability story that ties the entire catalog together. [\[github.com\]](https://github.com/github/spec-kit/blob/main/docs/reference/bundles.md)
### The Workflow Engine Matures
Beyond converge and bundles, June was the month the **workflow engine grew up**. The **step catalog** (#2394) made steps community-distributable; the **`init` step** (#2838) let workflows bootstrap projects; **JSON output** (#2963) and the **`from_json` filter** (#2961) made step outputs consumable as typed data; and the **`bug-assess`** agentic workflow (#3023) became the first shipped end-to-end automation built on the engine. Late-month hardening added bounded-concurrency fan-out (#3224), quote-aware expression parsing (#3232, #3197), stricter gate and `wait_for` validation (#3233, #3225), and correct non-zero exit codes on failed or aborted runs (#2959). The engine that began as a fixed seven-step sequence is now a programmable, community-extensible automation substrate. [\[github.com\]](https://github.com/github/spec-kit/releases)
### Architecture & Refactoring
The **`__init__.py` decomposition series** advanced from 4/8 to **7/8** during June. PR 5/8 co-located integration commands in the `integrations/` domain directory (#2720), PR 6/8 extracted preset command handlers into `presets/_commands.py` (#2826), and PR 7/8 moved extension command handlers into `extensions/_commands.py` (#3014). The systematic extraction continues to improve contributor onboarding and test isolation, with one part remaining. Dead HTTP helpers (`open_github_url`, `_StripAuthOnRedirect`) were removed following the preset URL-install hardening (#2883). [\[github.com\]](https://github.com/github/spec-kit/releases)
### Bug Fixes and Security
Twenty-five releases produced a heavy cadence of fixes, concentrated on **cross-platform parity** and **workflow robustness**. Windows/PowerShell saw the most attention: the PowerShell 5.1 init hang (#2938), UTF-8 stdout/stderr (#2817), stderr routing for `check-prerequisites.ps1` (#3123), case-sensitive branch-name acronym parity (#3129), and several bash-parity script fixes (#3196, #3198, #3230, #3231). Workflow correctness improved with loud failures on unknown expression filters (#3074), rejection of phantom permissions gates (#3079), and preserved commas inside quoted list literals (#3134). Long-standing bugs closed include the 233-day worktree branch-numbering repeat (#1066) and the extension-command registration gap on integration upgrade (#2886).
Security and supply-chain work was a distinct theme this month. **Preset URL installs were hardened against unsafe redirects** (#2911), **`run_command` now rejects `shell=True`** (#3132), **command-registration path handling was hardened** (#3088), **CI actions were pinned to commit SHAs with shellcheck added** (#3126), **catalog archives are verified by sha256 before install** (#3080), the **extension self-install path can no longer delete its source directory** (#2991), **per-extension failures are isolated** so one bad extension can't drop the rest (#2951), and **host-less catalog URLs are now rejected** in the base and preset validators (#3209). [\[github.com\]](https://github.com/github/spec-kit/releases)
### The Extension & Preset Ecosystem
The community extension catalog grew from 105 to **124 entries** during June — nineteen net additions across four steady weeks. Community presets grew from 21 to **23**.
The catalog also showed strong maintenance activity: **Linear Integration** advanced through several releases (to v0.7.0), **DocGuard — CDD Enforcement** progressed to v0.28.0, the **Superpowers** bridges continued rapid iteration, and **Architecture Guard**, **Security Review**, **Product Forge**, **MemoryLint**, and **Multi-Model Review** all shipped updates. New presets included **Command Density** and **SicarioSpec Core**, and the governance-preset family (a11y, agent-parity, cross-platform, iSAQB-architecture, architecture, security) received a coordinated round of updates. [\[github.com\]](https://github.github.io/spec-kit/community/extensions.html)
### Documentation & Docs Site
June closed several long-standing documentation gaps. A **guide for handling complex features** landed (#3004), and **evolving specs in existing projects** was formally documented (#2902, closing the 243-day #916). **Spec-persistence models** were documented (#2856), a **monorepo guide** was added (#3084), and **GitHub Copilot CLI guidance** joined the README (#2891). Reference docs for the new **bundles** and **integration catalog** subcommands were added (#3206, #3174), agent disclosure was strengthened to cover commits and per-round comments (#3071), and preset submissions now require a usage README with Spec Kit CLI syntax (#3104). [\[github.com\]](https://github.com/github/spec-kit/releases)
## Community & Content
### Microsoft's First-Party Endorsement
On **June 10**, the **Microsoft Developer Blog** published *"Spec-Driven Development: A Spec-First Approach to AI-Native Engineering"* by Apoorv Gupta (Principal Software Engineer, Microsoft) — the first first-party, non-maintainer post to present SDD and position **GitHub Spec Kit as the toolkit that operationalizes it**. The article covers the seven-step lifecycle and walks through three real greenfield and brownfield case studies, distilling the practice to a single line: **"spec quality = output quality."** Coming from Microsoft's own developer platform rather than the maintainers, it was the month's clearest signal that spec-driven development has moved from community experiment to institutionally endorsed practice. [\[developer.microsoft.com\]](https://developer.microsoft.com/blog/spec-driven-development-ai-native-engineering)
### Press and Industry Coverage
June was the **highest-volume coverage month on record — 75 substantive articles** across more than 25 languages.
**Xebia / XPRT Magazine #21** (Hidde de Smet & Emanuele Bartolesi, June 17) published a 32-minute full six-command walkthrough covering both greenfield and brownfield, honest about markdown-review overhead and where spec quality becomes the bottleneck. [\[xebia.com\]](https://xebia.com/blog/building-software-with-spec-kit/)
**Design News** (Jacob Beningo, June 26) published *"A Practical Guide to Spec-Driven Development with AI"*, explaining SDD for embedded engineers and highlighting Spec Kit as the agent-agnostic reference tool — notable for reaching an audience well outside the usual web-developer sphere. [\[designnews.com\]](https://www.designnews.com/embedded-systems/a-practical-guide-to-spec-driven-development-with-ai)
**SSOJet** (David Brown, June 26) surveyed seven SDD tools and named GitHub Spec Kit **"the category anchor and default agent-neutral pick."** [\[ssojet.com\]](https://ssojet.com/blog/best-spec-driven-development-tools)
**The Tokenizer** (Sairam Sundaresan, June 12), a curated AI newsletter, spotlighted `github/spec-kit` as the structured alternative to one-shot prompting alongside coverage of Spotify and DeepMind. [\[artofsaience.com\]](https://newsletter.artofsaience.com/p/spotifys-agent-context-layer-deepminds)
**FintechExtra** (June 1) published a factual v0.9.x release-notes summary covering the agent-context migration to an opt-in extension, UTF-8 CLI encoding fixes, JSON workflow output, and headless CLI dispatch. [\[fintechextra.com\]](https://www.fintechextra.com/news/spec-kit-v090-agent-context-migration-to-extension-608)
### Enterprise Adoption
**SNCF Connect & Tech** — the technology arm of France's national railway — went on the record in a **CIO Online** interview (Reynald Fléchaux, June 30). CTO Emmanuel Cordente reported **2–4× velocity gains** from adopting spec-driven development via open-source frameworks it named explicitly, including Spec Kit, while candidly flagging token-cost and governance concerns. It is one of the first named-enterprise, on-the-record velocity claims for SDD. [\[cio-online.com\]](https://www.cio-online.com/actualites/lire-emmanuel-cordente-sncf-connect-et-tech--avec-le-spec-driven-development-une-vitesse-multipliee-par-2-a-4-17120.html)
### Developer Articles and Blog Posts
June's 75 articles skewed heavily multilingual, with deep hands-on series in Chinese, Japanese, and Korean, and a strong current of "which tool should I choose?" comparisons.
Notable English-language articles:
- **Achraf Ben Alaya** (Azure MVP, June 28) published an honest .NET 10 / Blazor field report praising plan→tasks decomposition and the converge loop while flagging migration pitfalls and "overwhelming" markdown output. [\[achrafbenalaya.com\]](https://achrafbenalaya.com/2026/06/28/i-tried-github-spec-kit-an-honest-field-report/)
- **Particula Tech** (Sebastian Mondragon, June 18) compared Spec Kit, Kiro, and Tessl, calling Spec Kit the heaviest and most flexible (30+ agents) but "prone to review overload" — match tool weight to task. [\[particula.tech\]](https://particula.tech/blog/spec-driven-development-tools-spec-kit-vs-kiro-vs-tessl)
- **ToolTwist** (Portia Canlas, June 10) published a CxO field guide to BMAD, OpenSpec, and Spec Kit, concluding "none is best" and calling Spec Kit the **safe default for scaling teams**. [\[tooltwist.com\]](https://tooltwist.com/insights/spec-driven-frameworks-cxo-guide)
- **Allegro Tech** (Konrad Piechna, June 8) shared hard-won SDD best practices, threading Spec Kit's Specify→Plan→Implement→Validate model throughout. [\[blog.allegro.tech\]](https://blog.allegro.tech/2026/06/spec-driven-development-best-practices.html)
- **Yauhen Pyl** (June 3) published a hands-on scoring comparison rating Spec-Kit 2.77 vs OpenSpec 4.00 for brownfield/DX — praising the constitution model while calling it verbose and greenfield-biased. [\[ypyl.github.io\]](https://ypyl.github.io/programming/2026/06/03/openspec-vs-spec-kit-sdd.html)
Notable non-English coverage:
- **Japanese**: haru_iida published a thorough install + `/speckit.*` tutorial on Zenn from 6+ months of use. [\[zenn.dev\]](https://zenn.dev/haru_iida/articles/github-spec-kit-guide) A Qiita piece by IBM's Tomoyuki Hori documented integrating Spec Kit into the IBM Bob IDE. [\[qiita.com\]](https://qiita.com/Tomoyuki_Hori/items/eb0b1db560ba804cf8ac)
- **Chinese**: 掘金 (juejin.cn) ran multiple three-way "Spec Kit vs OpenSpec vs Superpowers" decision guides, and 腾讯云 published a balanced "spec as scaffolding vs single truth" analysis. [\[juejin.cn\]](https://juejin.cn/post/7657070407262421007)
- **Korean**: velog and Naver carried a wave of hands-on build logs and honest "is it too heavy?" critiques, including a full Claude Code + Spec-Kit end-to-end build. [\[velog.io\]](https://velog.io/@yono/GitHub-Spec-Kit%EC%9C%BC%EB%A1%9C-Spec-Driven-Development-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0)
- **Russian**: a vc.ru field report trialed Spec Kit across four projects, concluding roughly 30% of the author's work suits it — strong on greenfield, weak on research and existing code. [\[vc.ru\]](https://vc.ru/ai/2974391-opyt-ispolzovaniya-spec-kit-na-proyektakh)
Coverage also appeared on TabNews (Portuguese), Habr and CSDN, note.com, Substack (multiple), Medium, DEV Community, Design News, and company engineering blogs — the broadest linguistic spread yet recorded.
Across June's record article volume, a consistent framing emerged: spec-driven development is now an established category, and Spec Kit is its reference implementation. SSOJet called it "the category anchor," Design News and multiple comparison pieces called it the agent-neutral default, and ToolTwist's CxO guide named it the "safe default for scaling teams." The Microsoft Developer Blog post and the SNCF enterprise interview extended that framing beyond the developer press into institutional and enterprise contexts. [\[ssojet.com\]](https://ssojet.com/blog/best-spec-driven-development-tools)
### Competitive Landscape
The "which SDD tool?" comparison became June's dominant content genre, almost always featuring the same field: **Spec Kit, OpenSpec, Superpowers, BMAD, Kiro, Tessl, and GSD**. The recurring conclusion — from ToolTwist, BrainGrid, Particula Tech, and numerous multilingual surveys — was that the *practice* matters more than the tool, with Spec Kit positioned as the portable, community-driven, agent-agnostic default and competitors differentiating on brownfield ergonomics and drift management. Balanced reviews were consistent about the trade-off: Spec Kit is the heaviest and most flexible option (30+ agents, a full constitution/lifecycle model), which brings both the widest capability surface and the most review overhead. Hands-on scoring pieces (ypyl, vc.ru) rated it strong on greenfield and multi-scenario work and weaker on research tasks and incremental brownfield edits — precisely the gaps the `/speckit.converge` loop and the growing brownfield/drift extension ecosystem are built to close. [\[tooltwist.com\]](https://tooltwist.com/insights/spec-driven-frameworks-cxo-guide)
## Roadmap
Areas under discussion or in progress for future development:
- **The convergence loop** — `/speckit.converge` (#3001) is the core's direct answer to the drift-and-verification concern raised in nearly every review. Expect the append-only convergence model to deepen, and the community drift/verification extensions (Golden Demo, Spec Trace, Coding Standards Drift Control) to keep feeding requirements upstream. [\[github.com\]](https://github.com/github/spec-kit/blob/main/docs/quickstart.md)
- **The bundle subsystem** — `specify bundle` (#3070) establishes role-based distribution as a first-class primitive. With a community submission path now open (#3162) and four example roles shipped, curation, trust signals (`verified` vs `community`), and version-pin enforcement become the next areas to mature. [\[github.com\]](https://github.com/github/spec-kit/blob/main/docs/reference/bundles.md)
- **A programmable workflow platform** — with the step catalog, JSON output, and `from_json` filter, workflows are now community-extensible and scriptable. The open question is discoverability and pull: the step catalog is new, and adoption will show whether standalone workflow authoring becomes a real ecosystem or stays a power-user niche. [\[github.com\]](https://github.com/github/spec-kit/releases)
- **PyPI publishing** — a publishing workflow and README metadata landed (#2915, closing #2623), but official PyPI distribution is not yet the recommended install path; `uv tool install` and git remain canonical. Completing and hardening this reduces friction for restricted/air-gapped environments. [\[github.com\]](https://github.com/github/spec-kit/releases)
- **CLI architecture cleanup** — the `__init__.py` decomposition reached 7/8 (extensions/_commands.py, #3014), with one part remaining. The payoff is contributor onboarding and test isolation. [\[github.com\]](https://github.com/github/spec-kit/releases)
- **Toward a stable release** — v0.10.0's removal of `--no-git` and the git extension going opt-in was the first real breaking change, and the run to v0.12 reflects sustained pre-1.0 momentum. Expect continued API stabilization as the surface (bundles, workflows, converge) settles. [\[github.com\]](https://github.com/github/spec-kit/releases)
- **Experience simplification** — review overload, ceremony for small tasks, and verbose markdown output remain the most-cited concerns across June's balanced reviews (Particula Tech, ypyl, vc.ru, multiple Korean and Japanese pieces). The lean preset, TinySpec, `/speckit.converge`, and role bundles provide answers; surfacing them to new users is the ongoing opportunity. [\[particula.tech\]](https://particula.tech/blog/spec-driven-development-tools-spec-kit-vs-kiro-vs-tessl)
@@ -76,5 +76,3 @@ Areas under discussion or in progress for future development:
- **Continued agent expansion** -- seven new agents were added in March alone. The agent-agnostic design means support for emerging tools can be added by anyone. [\[byteiota.com\]](https://byteiota.com/spec-driven-development-kills-vibe-coding-march-2026/)
- **Experience simplification** -- the preset system, custom workflows, and growing walkthrough library lower the learning curve, but extension discoverability will need a more robust solution as the catalog grows. [\[github.com\]](https://github.com/github/spec-kit/releases)
- **Toward a stable release** -- nine releases in one month reflects pre-1.0 momentum. Reaching 1.0 will require stabilizing the extension and preset APIs and ensuring backward compatibility across the agent and extension surface area. [\[github.com\]](https://github.com/github/spec-kit/blob/main/newsletters/2026-February.md)
@@ -19,7 +19,7 @@ Before publishing a preset, ensure you have:
1.**Valid Preset**: A working preset with a valid `preset.yml` manifest
2.**Git Repository**: Preset hosted on GitHub (or other public git hosting)
3.**Documentation**: README.md with description and usage instructions
3.**Documentation**: A preset-scoped README.md that explains how to use **this preset**, including a valid `specify preset add ...` install command (see [Usage README Requirements](#usage-readme-requirements))
4.**License**: Open source license file (MIT, Apache 2.0, etc.)
"description":"Governs TDD with coverage-complete BDD/ATDD Gherkin scenarios, explicit suite ownership, professional test reports, traceability, and risk-based quality gates.",
@@ -181,7 +191,18 @@ function Get-FeaturePathsEnv {
[Console]::Error.WriteLine("ERROR: Feature directory not found. Set SPECIFY_FEATURE_DIRECTORY or run the specify command to create .specify/feature.json.")
exit1
}
# When no branch context exists (no SPECIFY_FEATURE, feature resolved via
# SPECIFY_FEATURE_DIRECTORY or feature.json), fall back to the feature
# directory basename so CURRENT_BRANCH is a usable identifier rather than
# an empty, misleading value (issue #3026).
if(-not$currentBranch){
# TrimEnd (not [Path]::TrimEndingDirectorySeparator, which is .NET Core
# only) keeps this working on Windows PowerShell 5.1 / .NET Framework.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.