The Claude Code integration installs skills into `.claude/skills` (see
integrations/claude: `"dir": ".claude/skills"`), and the "what gets kept"
list earlier in this same doc already says `.claude/skills/`. But three
troubleshooting/reference spots still point users at `.claude/commands/`,
which does not exist for a Claude Code install -- so the "verify files
exist" checks list an empty/missing directory. Correct all three to
`.claude/skills/`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(auth): normalize whitespace in auth-config env-var/id references at store time
token_env, client_secret_env, tenant_id, and client_id were VALIDATED on
their .strip()ed form but STORED raw, so an accidentally padded value passed
validation yet silently broke the downstream verbatim os.environ.get(name) /
OAuth-URL lookups — load_auth_config succeeded but resolve_token returned
None and the request quietly downgraded to unauthenticated (401/403) with no
diagnostic.
Normalize these whitespace-insignificant string references with a _norm
helper at store time, mirroring how `hosts` is already normalized
(h.strip().lower()). `token` is unchanged (already stripped at resolve time).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(auth): cover tenant_id/client_id/client_secret_env normalization
Address review: the regression test only covered token_env, but the fix also
normalizes tenant_id, client_id, and client_secret_env. Add a padded
azure-ad entry asserting all three are stored stripped.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
execute()/resume() run UNVALIDATED definitions (load_workflow does not
validate). WorkflowDefinition stores `inputs` raw, so a non-mapping
`inputs:` block (bare `inputs:` -> None, or `inputs: []`) crashed
_resolve_inputs at `for name, input_def in definition.inputs.items()` with
AttributeError, aborting the whole run.
Return {} when inputs is not a mapping, mirroring validate_workflow's own
`isinstance(definition.inputs, dict)` check. Protects both call sites
(execute and resume); normal dict resolution is unchanged.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: clarify shell-step interpolation safety
Shell step `run` fields are executed by the system shell and `{{ ... }}`
expressions are substituted as raw, unquoted text. Document that untrusted
sources — workflow `inputs.*` and prior-step output, including AI-generated
`prompt` output — must be quoted, enum-constrained, validated, or gated before
they reach a `run` field.
- docs/reference/workflows.md: add an "Interpolation and shell safety" section.
- workflows/README.md: add a warning under the Shell Steps example, link to the
new section, and quote the `inputs.project_dir` example.
- workflows/PUBLISHING.md: strengthen the interpolation guidance and call out
prior-step/agent output as untrusted.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c0fd6900-69bf-4fcb-b377-de37f98f5835
* docs: correct shell-step interpolation guidance
Address review feedback that the previous wording over-promised. Clarify that
none of the mitigations neutralise a hostile interpolated value:
- Quoting is not a security boundary — there is no shell-escaping filter, and a
value containing the matching quote can break out. Present quoting as
correctness handling for already-constrained values only.
- Remove the "pass data via environment or files" guidance: ShellStep has no
`env` mapping (it only copies the process environment and sets
SPECKIT_WORKFLOW_DIR), so that transport does not exist.
- Drop the claim that routing through a command/prompt step validates or safely
binds a value; it does not.
- Correct the gate guidance: a gate renders only its own message/show_file and
does not inspect, resolve, or sanitise the following step. Authors must
surface the exact command/data in the gate themselves, and approval does not
neutralise an injectable interpolation.
Frame constraining values at the source (enum/allowlist) as the only reliable
control, and keeping unconstrained values out of `run` fields entirely.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c0fd6900-69bf-4fcb-b377-de37f98f5835
* docs: remove unsafe interpolation from example and gate guidance
Address further review feedback:
- workflows/README.md: the shell example interpolated an unconstrained path
into shell source, which contradicted the warning beneath it. Shell steps
already run from the project root, so drop the `cd '{{ inputs.project_dir }}'`
prefix and model a plain `run: "npm test"` with no interpolation.
- docs/reference/workflows.md: GateStep prints `message` verbatim with no
control-character stripping (stripping applies only to `show_file` path and
contents), so recommending that authors surface untrusted data in `message`
was itself unsafe — agent/caller output could inject terminal escapes to
alter or hide the prompt. Direct authors to keep `message` to trusted text
and surface untrusted material via `show_file`, whose contents are stripped.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c0fd6900-69bf-4fcb-b377-de37f98f5835
* docs: use correct prompt-step output key in example
A `prompt` step stores agent-generated text under `output.stdout`, not
`output.value`, so the example expression `{{ steps.plan.output.value }}`
would resolve to None. Reference `output.stdout` so the example correctly
demonstrates untrusted agent output flowing into a shell step.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c0fd6900-69bf-4fcb-b377-de37f98f5835
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c0fd6900-69bf-4fcb-b377-de37f98f5835
Accessing the parsed authority (via urlparse/.hostname) raises ValueError
on a malformed bracketed host, e.g. https://[not-an-ip]/..., mirroring
the existing .port guard below. download_url is server-controlled (a
catalog download_url payload), so the function's resolve-or-return-None
contract must hold rather than leaking a raw traceback to the caller.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(integrations): declare PiIntegration multi_install_safe
PiIntegration writes only to its isolated, static root .pi/prompts,
disjoint from every other integration, yet never declared
multi_install_safe — so it inherited the IntegrationBase default False,
leaving `specify integration status` in a permanent unsafe-multi-install
ERROR state when pi is co-installed alongside another agent.
Add `multi_install_safe = True`, mirroring the isolated MarkdownIntegration
cohort (qwen, shai, qodercli) and the kiro-cli #3471 fix. The parametrized
registry isolation contracts auto-include pi and pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(integrations): list pi in the multi-install-safe reference table
Declaring PiIntegration multi_install_safe means the reference table in
docs/reference/integrations.md (which states it lists all currently
declared multi-install-safe integrations) should include it. Add the
alphabetized pi row with its .pi/prompts isolation path.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
run_command() enforces a list[str] argv contract, so a shell parameter
served no purpose beyond keeping an unnecessary shell-injection surface
that a future refactor could re-enable. Remove the parameter (and its
now-dead ValueError guard) so shell=False is the only possible behavior.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 74a1bd02-f6cd-412a-b5a8-a7767a5e058d
Treat an explicit feature number as a preference when an existing spec directory already uses that prefix. Advance to the next available spec prefix and warn without fetching or scanning git branches.
Keep Bash, PowerShell, and Python variants aligned. Preserve 64-bit numbering, timestamp mode, dry-run output, matching-file behavior, and exact-directory reuse through the allow-existing option.
Assisted-by: Codex (model: GPT-5, autonomous)
* chore: bump version to 0.14.1
* chore: begin 0.14.2.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(bundler): InstallResult.changed counts uninstalled as a change
The `changed` property only considered `installed` and `refreshed`, omitting
`uninstalled`. A `bundle update` whose new manifest drops components (removing
them via the refresh path) with no new install/refresh produces
installed=[], refreshed=[], uninstalled=[dropped set] — yet `changed` returned
False, misreporting a mutating update as a no-op.
Include `uninstalled` in the disjunction (it is the third mutating outcome
list on the same dataclass, also the sole output of the remove_bundle path).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: pin ruff to 0.15.0 to avoid 0.16.0 default-ruleset breakage
ruff 0.16.0 expanded its default rule set from ~59 to ~413 rules,
causing the unpinned `uvx ruff check` step to report ~1475 pre-existing
violations unrelated to this change. Pin to 0.15.0 to restore green lint.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
* Update Cross-Platform Governance preset to v0.2.1
Update cross-platform-governance preset submitted by @hindermath to:
- presets/catalog.community.json (version, download_url, documentation, description, updated_at)
- docs/community/presets.md community presets table
Closes#3683
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* ci: pin ruff to 0.15.0 to avoid 0.16.0 default-ruleset breakage
ruff 0.16.0 expanded its default rule set from ~59 to ~413 rules,
causing the unpinned `uvx ruff check` step to report 1476 pre-existing
violations. Pin to 0.15.0 to restore green lint until the codebase is
audited against the new defaults.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, 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: Manfred Riem <15701806+mnriem@users.noreply.github.com>
`workflow info` rendered each step as `→ <id> [<type>]`, but console.print
has Rich markup enabled, so `[<type>]` was parsed as a style tag named after
the step type (command/gate/prompt/…) and silently swallowed — every step
printed as `→ <id> ` with the type gone.
Escape the literal bracket with `\[` (and escape id/type via _escape_markup,
as the sibling workflow_list does), so Rich renders `[<type>]` literally.
Mirrors the in-file `\[disabled]` precedent.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_apply_filter parsed a name(arg) filter with an UNANCHORED regex
(re.match(r"(\w+)\((.+)\)")), so any tokens after the closing paren were
silently discarded. Because _evaluate_simple_expression splits the top-level
pipe before comparison/boolean operators, `count | default(0) > 5` was split
into value `count` and filter segment `default(0) > 5`; the segment matched
as `default(0)` and `> 5` vanished — the filter's value was returned as the
whole expression, giving a silently wrong result.
Use re.fullmatch so a mis-wired segment falls through to the existing
"unsupported form" ValueError, mirroring the from_json branch's strict
trailing-token handling. The greedy `.+` still matches legitimate forms
(literal `)` / `|` inside quoted args), so registered/chained/quoted-pipe
filters are unaffected.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(extensions): parse SKILL.md on the --- delimiter line during removal
ExtensionManager._unregister_extension_skills verified an installed skill
before deleting it by reading metadata.source back from its SKILL.md with a
raw split("---", 2). That substring split stops at the first "---" anywhere
after the opening delimiter, including one embedded in a command description
(e.g. "Separate sections with --- markers"). The frontmatter was then
truncated mid-value, metadata.source parsed empty, the skill looked
unrelated, and its directory was left orphaned on uninstall.
Parse on the "---" delimiter *line* instead, reusing CommandRegistrar.
parse_frontmatter (the line-anchored parser from #3590) in both the fast
(registry-driven) and fallback (directory-scan) removal paths.
Add a regression test that installs an extension whose command description
contains "---", removes it, and asserts the skill directory is gone. Fails
before the fix (dir orphaned), passes after.
* test: cover the fallback scan branch for the --- SKILL.md parse
Copilot noted the new regression test only exercised the fast removal
path (skills_project keeps ai_skills enabled, so remove() resolves the
skills dir directly). Add test_skills_removed_with_dashes_via_fallback_scan,
which deletes init-options.json after install so _get_skills_dir() returns
None and removal takes the fallback directory-scan branch. That branch
re-reads metadata.source with an independently duplicated parser; reverting
it to the old substring split now fails this test (dir orphaned) while the
fast-path test still passes.
* fix(cli): guard lazy .hostname ValueError in extension/preset add --from
`extension add --from <url>` and `preset add --from <url>` validated the URL
by reading `parsed.hostname` OUTSIDE their `try/except ValueError` guards. A
bracketed-but-invalid IPv6 authority (e.g. "https://[not-an-ip]/x.zip") parses
cleanly under urlparse() on Python < 3.14 and only raises ValueError lazily on
the first .hostname access. On the interpreters spec-kit supports (>=3.11) that
raw ValueError leaked past the CLI, printing an uncaught traceback instead of
the clean "Invalid URL" error. (The raise moved eager into urlparse() only in
3.14.) Same bug class as the catalog/download fixes #3433/#3435/#3437/#3577.
- extensions/_commands.py: read parsed.hostname inside the existing try and
reuse it for the localhost check.
- presets/_commands.py: guard the up-front `urlparse(from_url).hostname` read
(preserves the "Invalid URL" message), and harden the nested
`_is_allowed_download_url` to take a URL string and parse+read .hostname
inside its own try/except -> returns False on malformed input. This also
covers the redirect-validator and final-URL (post-redirect) checks, where the
URL is server-controlled.
Regression tests for each command: a bracketed-non-IP URL, plus a monkeypatched
lazy-.hostname raiser that reproduces the pre-3.14 shape independently of the
running interpreter (fails with a raw ValueError before the fix, verified via
test-the-test).
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(cli): address Copilot review on --from URL guard comments/tests
Copilot's review on #3651 flagged two accuracy problems:
1. The guard comments asserted a specific (and incorrect) CPython version
history -- that "https://[not-an-ip]/..." parses cleanly under urlparse()
on Python < 3.14 and only raises ValueError lazily on the first .hostname
access. In fact the eager bracketed-host check (gh-103848, CVE-2024-11168)
was backported to the 3.11 branch and shipped in 3.11.4, so on every
interpreter spec-kit supports (>=3.11) that URL is rejected eagerly at
urlparse(). Reworded the three source comments to state the guard as a
defensive policy (parsing OR the .hostname read can raise ValueError, guard
both) without asserting version history.
2. The two monkeypatched lazy-.hostname tests were described as reproducing
"the exact production path" / "the Python < 3.14 shape". They are synthetic
defensive cases. Relabeled them as synthetic defensive coverage that does
not reproduce any specific CPython behavior, and dropped the version-history
claims from the bracketed-non-IP test docstrings.
The second-round suggestion (_is_allowed_download_url(final_url) instead of
_is_allowed_download_url(_urlparse(final_url))) was already applied in the
original commit.
Behavior unchanged; comments/docstrings only. URL-guard tests 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>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(bundler): reject a top-level non-mapping bundle-catalogs.yml in _merge_config
_merge_config silently ignored a top-level non-mapping document (a YAML list
or scalar) — `data.get("catalogs") if isinstance(data, dict) else None` made
it fall through to the built-in default stack — while the sibling reader of
the SAME file (commands_impl/catalog_config._read) raises "expected a mapping
at the top level". #3623 already made the inner non-list `catalogs` value
agree between the two readers; this closes the remaining top-level-shape gap
so both readers reject the same malformed documents.
An empty file (load_yaml coerces to {}), absent `catalogs`, and `catalogs: []`
all remain no-ops.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(bundler): reject FALSY non-mapping catalog configs (parse raw, not load_yaml)
Address review (Copilot on #3659): the top-level guard used the shared
load_yaml, whose `yaml.safe_load(...) or {}` coerces a FALSY top-level
document ([], false, 0, '') to {} BEFORE the isinstance check — so those
malformed configs silently fell back to the built-in defaults instead of
raising. Only truthy non-mappings ([a,b], 42) were caught.
Parse the raw document in both readers of bundle-catalogs.yml
(models/catalog._merge_config AND commands_impl/catalog_config._read):
an empty document (None) stays a no-op, but every non-mapping top level —
falsy or truthy — now raises "expected a mapping at the top level". This
keeps the two readers genuinely consistent. Tests cover the falsy cases for
both.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(bundler): correct load_yaml so only empty documents become {} (not falsy non-mappings)
Address review (Copilot re-review of #3659): the previous fix duplicated
YAML parsing + exception wrapping inline in two readers, bypassing the
centralized yamlio helper. Instead, correct the root cause in load_yaml.
load_yaml did `yaml.safe_load(...) or {}`, which coerced ANY falsy result
(None empty-doc, but also [], false, 0, '') to {} — contradicting its own
docstring ("{} for an empty document") and hiding malformed non-mapping
configs from callers' shape guards. Change to `{} if data is None else data`
so only an empty document becomes {}; a non-mapping top level is returned
as-parsed.
Revert the inline raw-parse in models/catalog._merge_config and
commands_impl/catalog_config._read back to the centralized load_yaml; their
existing `isinstance(data, dict)` guards now correctly reject falsy
non-mappings too. All three load_yaml callers (these two + manifest.from_dict)
already guard the top-level shape, so none regresses. Falsy-case tests for
both readers retained.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(bundler): distinguish an empty YAML document from an explicit null in load_yaml
Address review (Copilot on #3659): yaml.safe_load returns None for BOTH an
empty document AND an explicit null scalar (`null`/`~`), so mapping None to {}
still let a top-level null bundle-catalogs.yml fall back to defaults instead of
being rejected by the mapping guard.
Use yaml.compose (which yields a node only for a non-empty document) to tell
the two apart: a truly empty document becomes {}, while an explicit null is
returned as None so the callers' isinstance(dict) guard rejects it like any
other non-mapping. Drop the now-incorrect `if data is None: return []`
short-circuit in catalog_config._read so an explicit null reaches that guard.
Tests cover null/~ for both readers plus empty/comment-only no-op.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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): declare OmpIntegration multi_install_safe
OmpIntegration is a plain MarkdownIntegration whose files live only under
its isolated, static root .omp/commands/, disjoint from every other
integration. But it never declared multi_install_safe, so it inherited the
IntegrationBase default False — leaving `specify integration status` in a
permanent unsafe-multi-install ERROR state whenever omp is co-installed
alongside another agent, with no acknowledgment path.
Add `multi_install_safe = True`, mirroring the isolated MarkdownIntegration
cohort (qwen, shai, qodercli, junie, kilocode) and the kiro-cli #3471 fix.
The parametrized registry isolation contracts auto-include omp once the flag
is set and pass (.omp/commands is isolated and its manifest disjoint).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(integrations): list omp in the multi-install-safe reference table
Declaring OmpIntegration multi_install_safe means the reference table in
docs/reference/integrations.md (which states it lists all currently
declared multi-install-safe integrations) should include it. Add the
alphabetized omp row with its .omp/commands isolation path.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(git-extension): add configurable Conventional Commit support
Adds a commit_style option (fixed | conventional) to the git
extension's auto-commit config. When set to conventional, the
speckit.git.commit hook instructs the agent to generate a Conventional
Commit message from the diff and pass it to auto-commit.sh /
auto-commit.ps1 as an explicit argument. If no message is supplied in
conventional mode, the scripts fail loudly (stderr + exit 1) instead of
silently falling back to the fixed message, but still short-circuit
cleanly when there are no changes to commit.
- extensions/git/config-template.yml, git-config.yml: new
commit_style: fixed (default) / conventional option.
- extensions/git/scripts/bash/auto-commit.sh: optional
[generated_message] arg, commit_style parsing, conventional-mode
enforcement.
- extensions/git/scripts/powershell/auto-commit.ps1: mirrored
PowerShell implementation.
- extensions/git/commands/speckit.git.commit.md: documents commit
message styles and updated execution/config guidance.
- extensions/git/README.md: documents the new option.
- tests/extensions/git/test_git_extension.py: regression tests for
fixed default, conventional success, conventional missing-message
failure, and no-changes short-circuit (bash + PowerShell).
Fixes#3390
Assisted-by: GitHub Copilot (model: claude-sonnet-5, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(tests): check combined stdout+stderr for conventional commit_style failure test
Write-Warning output stream placement is not deterministic across pwsh
versions/platforms (observed failing on macOS CI). Match the existing
pattern used elsewhere in this file (e.g.
test_not_a_repo_still_detected_with_autocrlf) by asserting against the
combined stdout+stderr instead of stderr alone.
Assisted-by: GitHub Copilot (model: claude-sonnet-5, autonomous)
* fix(git-extension): strip YAML inline comments from commit_style value
Copilot review feedback on PR #3413 identified that commit_style parsing
didn't strip trailing YAML inline comments (e.g. "commit_style: conventional
# team standard"), causing the value to retain a trailing comment fragment
and silently skip conventional-mode enforcement.
- bash: fix the inline-comment strip regex to use a proper {1,} interval
so multiple spaces before '#' are consumed together with the comment,
preventing a stray trailing quote character from surviving quote-strip
when the value is quoted (e.g. commit_style: "conventional" # x).
- powershell: already handled this correctly via \s+#.*$ + Trim(); no
behavior change needed there.
- tests: add regression coverage for commit_style values with trailing
inline comments (bash + pwsh), and a pwsh regression test for the
no-changes short-circuit ordering, per additional Copilot suggestion.
Assisted-by: GitHub Copilot (model: claude-sonnet-5, autonomous)
* fix(git-extension): validate commit_style and reword abort message per review
Address Copilot review feedback on PR #3413:
- Validate commit_style against the documented fixed/conventional values;
an unrecognized value now warns and falls back to fixed instead of
silently mis-parsing.
- Reword the conventional-mode-without-generated-message message from
'skipped auto-commit' to 'aborting auto-commit' since the script exits
1 (a failure, not a skip), and include the actionable remediation.
- Add regression tests (bash + pwsh) covering the unknown commit_style
fallback.
Assisted-by: GitHub Copilot (model: claude-sonnet-5, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(git-extension): limit commit_style parsing to first match in config
Address Copilot review feedback on PR #3413: grep '^commit_style:' without
-m1 could concatenate values if a config file accidentally contains
multiple commit_style lines (e.g. from a bad merge/manual edit), causing
an unexpected fallback to 'fixed'. Limit to the first match and add a
regression test covering duplicate commit_style lines.
Assisted-by: GitHub Copilot (model: claude-sonnet-5, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(git-extension): avoid shell interpolation of generated commit messages
- Remove r -d '[:space:]' from commit_style parsing in auto-commit.sh:
it stripped ALL whitespace (not just leading/trailing), so
commit_style: con ventional was silently normalized to conventional
instead of being rejected as unknown (PowerShell version already
rejected it correctly).
- Add a file-based message-passing channel to both auto-commit scripts:
--message-file <path> (bash) / -MessageFile <path> (PowerShell).
Agent-generated commit messages may contain quotes, $(...), or
backticks; passing them as a shell argument risked command injection
if ever inlined into a shell command string. The new flag reads the
message from a file instead, so untrusted content never touches a
shell command line. The raw positional-argument form is kept for
backward compatibility.
- Update speckit.git.commit.md to instruct the agent to write the
generated message to a temp file (via its file-editing tool) and pass
the file path, explicitly warning against inlining the message into a
shell command string.
- Add test coverage: explicit commit_style: fixed (previously only the
absent-key default was tested), --message-file/-MessageFile success
path (including injection-shaped content), and missing-file error path,
for both bash and PowerShell suites.
Assisted-by: GitHub Copilot (model: claude-sonnet-5, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(git-extension): exclude --message-file transport file from staging
The temp file passed via --message-file / -MessageFile was read but left
in the worktree. If written inside the project (as an agent's file-editing
tool would naturally do), git add . staged it into the commit, and its
mere presence as an untracked file could also defeat the no-changes
short-circuit, causing a spurious commit containing only that file.
Remove the file immediately after its content is captured, before the
change-detection check and before staging. Add bash + pwsh regression
tests covering both scenarios.
Assisted-by: GitHub Copilot (model: claude-sonnet-5, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
After `specify extension add`, the "Provided commands" summary hyphenated
command names only for Cline. For a Forge project the names were printed in
dotted form (e.g. `speckit.test-ext.hello`), but Forge registers them
hyphenated (`speckit-test-ext-hello`), so the printed names didn't match
what the user actually invokes in Forge.
Extend the existing Cline handling to Forge via `format_forge_command_name`,
completing the Forge command-name parity already fixed for hook invocations
(#3641) and the init next-steps panel (#3642).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CatalogEntry.from_dict used `data.get("requires") or {}` and
`data.get("provides") or {}`, so a FALSY non-mapping ([], '', 0, false) was
coerced to {} before the isinstance guard — a corrupt catalog entry passed
silently. Only a truthy non-mapping was rejected.
Handle None explicitly and reject every other non-mapping, mirroring the
merged manifest requires/provides/integration guards (#3629, #3661).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
load_records and InstalledBundleRecord.from_dict defaulted their list fields
with `data.get(...) or []` BEFORE the isinstance(list) guard, so a FALSY
non-list value (0, '', False, {}) was coerced to [] and the guard became dead
code — a corrupt .specify/bundle-records.json was silently read as "no
bundles"/"no components" instead of raising. Only an absent/None value should
mean empty.
Handle None explicitly and reject every other non-list, mirroring the merged
requires/provides/integration guards (#3629, #3661) and the catalog_config
sibling reader.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(extensions): clarify agent-context README and add config examples
Rewrite the agent-context extension README to read as plain prose
instead of a bullet dump, and add the missing install/disable
commands (specify extension add/disable/enable agent-context).
Add inline example comments to agent-context-config.yml for
context_file/context_files.
* docs(agent-context): clarify config documentation
- Reformat comments to flow as single-line paragraphs instead of multi-line breaks
- Add "WHAT" sections describing each configuration option's purpose
- Add "REQUIREMENT" sections specifying if options are optional or required
- Add explicit EXAMPLE sections for context_markers configuration
- Improve clarity of context_file and context_files option descriptions
* docs(agent-context): fix GitHub casing, clarify config
- Fix "Github" -> "GitHub" casing in README issues link
- Clarify agent-context-config.yml comments on context_file/context_files behavior and precedence
* YAML indentation fix for context markers
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* docs(agent-context): simplify README config section
- Clarify config file path reference (.specify/extensions path vs repo path)
- Remove duplicated YAML example/field docs from README, point to config file directly
- Minor spacing fix in agent-context-config.yml comment
* docs(agent-context): clarify config file path in README
- Reference the installed .specify config path alongside the repo-relative link
* docs(agent-context): clarify install and marker requirement
- README: clarify install command must be run from an initialized Spec Kit project root
- config: correct context_markers requirement from REQUIRED to OPTIONAL
* Wording Fix
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* docs(agent-context): clarify context_file path rules
- Document that context_file/context_files are relative to the project root (directory containing .specify/)
- State the rejected path forms (absolute paths, backslash separators, .. segments) directly in each field's WHAT comment
* Updated supported invocation syntaxes
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Extension Disable Clarification
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* docs(agent-context): document .mdc frontmatter exception
- Clarify that .mdc files get alwaysApply: true set in frontmatter, outside the managed marker block
- Fix "Everything else is untouched" wording so it doesn't contradict the exception right above it
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* chore: bump version to 0.14.0
* chore: begin 0.14.1.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* docs: add spec-kit-copilot to community friends
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c4ad8a5e-3ddb-44b2-b8ec-4f438c636b61
* docs: note first-party exception for spec-kit-copilot
Clarify the page disclaimer so it covers first-party GitHub projects, and
mark spec-kit-copilot as a first-party GitHub project.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c4ad8a5e-3ddb-44b2-b8ec-4f438c636b61
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c4ad8a5e-3ddb-44b2-b8ec-4f438c636b61
with_integration_setting recomputed invoke_separator from the raw
parsed_options argument. When only script_type changes (parsed_options and
raw_options both None), the previously-stored parsed_options are retained on
the setting, but the separator was derived from the None argument — dropping
an options-dependent separator (e.g. Copilot --skills -> "-") back to the
default ".", desynchronizing invoke_separator from the stored options.
Derive the separator from current.get("parsed_options") — the options
actually stored after the update — so it stays consistent in every branch.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_traverse_and_apply's insert_after loop iterated reversed(edits) over the
flat per-anchor edit list. The reversal is only meant to place a
higher-priority OVERLAY closer to the anchor (mirroring insert_before's
winner-closest behaviour), but reversing the flat list also flipped the
declared order of multiple insert_after edits authored within a SINGLE
overlay: [insert_after a->x, insert_after a->y] produced [a, y, x, b]
instead of [a, x, y, b]. insert_before (a forward loop) already preserves
order, so the two operations were asymmetric.
Group contiguous same-layer edits and reverse the GROUP order only, keeping
each overlay's own inserts in declared order. Cross-overlay priority is
unchanged (higher-priority overlay still lands closest to the anchor).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(bundler): reject falsy non-mapping requires/provides in manifest from_dict
BundleManifest.from_dict used `data.get("requires") or {}` and
`data.get("provides") or {}`, so a FALSY non-mapping value ([], '', 0,
false) was coerced to {} BEFORE the isinstance guard — a malformed manifest
passed validation as one that requires/provides nothing. Only a truthy
non-mapping (e.g. "extensions") was rejected.
Handle None explicitly (default to {}) and reject every other non-mapping,
matching the sibling 'integration' guard added in #3629. Absent fields still
parse to the empty default.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(bundler): correct absent-optional-mapping regression assertion
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dump_yaml called yaml.safe_dump without allow_unicode=True, so non-ASCII
content was written as \xNN / \uXXXX escapes instead of literal UTF-8 — a
round-trip readability loss for bundle config. The centralized helper
_utils.dump_frontmatter and the extensions/presets config writers all pass
allow_unicode=True; align dump_yaml with them.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* tidy kiro-cli multi-install-safe declaration + add docs row and test
kiro-cli is declared multi_install_safe (it uses a fully isolated .kiro/
root, .kiro/prompts command dir, a stable '.' separator, and a dedicated
manifest — #3471). main ended up with the flag assigned twice in the class
body; this drops the bare duplicate and keeps a single declaration with the
comment explaining why it's safe.
also adds the missing kiro-cli row to the multi-install-safe table in
docs/reference/integrations.md, and a test asserting the flag is set (the
registry contract tests already enforce the actual path isolation against
every other safe integration).
* address review: drop agent-file column and duplicate test
per maintainer guidance on #3477: the Isolation table listed each
integration's agent-context file (AGENTS.md, CLAUDE.md, etc.) alongside
its command dir, which contradicted the safety definition (Copilot flagged
kiro-cli/codex both mapping to AGENTS.md). those context files are owned by
the optional agent-context extension, not by multi_install_safe — the flag
governs only the command directory + manifest.
- renamed the column to 'Command directory' and removed the agent-file
entries, so it lists only what each integration actually manages
- reworded the definition to note agent-context is a separate concern and
is not multi-install safe
- removed the duplicate test_declared_multi_install_safe (the existing
test_declares_multi_install_safe already asserts the same thing)
* address review: keep agent-root requirement; reframe agent-context targeting
- Restore 'static, unique agent root' alongside command directory in the
multi-install-safe definition — base.py and test_registry.py
(test_safe_integrations_have_distinct_agent_roots) enforce both.
- Reframe the agent-context note: multi_install_safe is an integration-level
declaration about command/skill paths, so describe context-file targeting
as independent of it rather than calling the extension 'not multi-install
safe'. The extension can even sync multiple anchors via context_files.
* address review: hoist multi_install_safe to top of class
Move the multi_install_safe = True declaration to the top of
KiroCliIntegration (right after key) so it is visible at the exact spot
the diff touched. The flag was never actually removed — it was declared
once further down the class — but placing it at the top makes the opt-in
unmistakable in review and keeps the single declaration. Verified the
integration still resolves multi_install_safe is True; 24 kiro-cli tests
pass.
The auto-commit bash and Python twins strip a leading/trailing quote from
the configured `message:` value with an end-of-string-anchored quote strip.
When the YAML value has trailing whitespace after the closing quote
(`message: "Done" `), the close-quote strip is anchored to end-of-string,
so it never matches the quote (spaces follow it). The commit message then
keeps a dangling quote and trailing spaces (`Done" `).
The PowerShell twin already .Trim()s before stripping, so it produced the
clean `Done`. This left the three script variants out of parity. Trim the
value before stripping quotes in the bash and Python twins so all three
agree.
Verified at the exact-code level: the old bash sed pipeline yields
`spec done" ` and the new one `spec done`; the Python _strip_quotes matches.
Add a parity regression test with trailing whitespace after the closing
quote (runs under CI where Git bash is resolvable).
_collect_files returned sorted(collected), i.e. pathlib.Path order, which is
platform-dependent: on Windows PurePath compares case-folded with backslash
separators, whereas the zip member NAMES are the canonical POSIX arcnames
(build_bundle: file_path.relative_to(bundle_dir).as_posix()). So the same
bundle built on Windows vs Linux/macOS produced archives whose members were
laid out in different order — not byte-for-byte identical across build hosts,
contradicting the packager's reproducible-build guarantee (fixed timestamps +
canonical modes).
Order by the same canonical POSIX-arcname key used to name members, so member
order is host-independent.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ClineIntegration defined its command-content transform as
post_process_content, but the overridable base hook is
IntegrationBase.post_process_command_content, which
CommandRegistrar.register_commands() dispatches to for every non-skills
integration. Because the names differed, Cline's method never overrode the
base hook, so extension/preset command files registered for Cline silently
ran the base no-op and never received Cline's dot-to-hyphen hook-command
note (_inject_hook_command_note) — the note that tells the agent to replace
dots with hyphens when invoking hook commands. (Handoff references are
already hyphenated independently by the registrar's _hyphenate_body_refs,
so that transform was unaffected; renaming simply makes Cline's own copy
run too, harmlessly, since both are idempotent.)
Rename to post_process_command_content (matching the base hook and the
post_process_skill_content convention used by claude/copilot/agy/kimi/
droid/vibe) and update the single internal caller in setup(). Cline's own
setup() post-processing of core commands is unchanged. No test referenced
the old name.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GateStep docstring said on_reject "controls abort / skip behaviour",
omitting the third value. validate() accepts 'abort', 'skip', or 'retry',
and execute() has a dedicated retry branch (returns PAUSED so the next
resume re-runs the gate) distinct from abort (FAILED) and skip (COMPLETED).
Add 'retry' to the docstring so it matches the same file's validate() and
execute() authority.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`specify init --script py` generated skills that invoke
`python3 .specify/scripts/python/<name>.py`, but the wheel's
force-include list only carried `scripts/bash` and `scripts/powershell`.
Installs from PyPI/Homebrew therefore shipped commands pointing at files
that were never packaged, leaving `--script py` non-functional while
`sh`/`ps` kept working.
Force-include `scripts/python` alongside the other two variants, and add
a contract test that asserts every script variant present in the repo is
bundled, so a future variant cannot be dropped the same way.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The wheel force-include mapped scripts/bash and scripts/powershell into
core_pack but omitted scripts/python. As a result, `specify init
--script py` laid down commands referencing
.specify/scripts/python/*.py while installing bash scripts, breaking
every command at its first setup step.
Add the scripts/python force-include mapping to mirror the other script
variants so the core Python scripts ship in the packaged wheel.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Copilot-Session: 1bc9c590-2c0c-4d88-bf3c-23f265cef82d
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The InitStep `script` field docstring claimed only 'sh' or 'ps', but the
step's own VALID_SCRIPT_TYPES = tuple(SCRIPT_TYPE_CHOICES.keys()) is
('sh', 'ps', 'py') and validate() accepts all three (its error message is
built from VALID_SCRIPT_TYPES). Update the docstring to list 'py' too, so
it no longer contradicts the same class's validate() authority.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
LingmaIntegration writes only to its isolated, static root .lingma/skills,
disjoint from every other integration, yet never declared
multi_install_safe — inheriting the IntegrationBase default False and
leaving `specify integration status` in a permanent unsafe-multi-install
ERROR state when lingma is co-installed alongside another agent.
Add `multi_install_safe = True`, mirroring the structurally-identical
trae/zcode SkillsIntegrations and the kiro-cli #3471 fix. The parametrized
registry isolation contracts auto-include lingma and pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(scripts): document the 'py' script type and sh/ps migration plan (#3284)
Bring remaining docs up to date with the Python (`py`) workflow-script
variant introduced in #3277, and record the retention/deprecation plan
for the shell variants.
- AGENTS.md: document the `scripts:` frontmatter (sh/ps/py), clarify the
`{SCRIPT}` placeholder resolution, and add a "Script Types and
Migration" section (why py is recommended, defaults, phased sh/ps
deprecation path). Note the Python agent-context variant.
- docs/quickstart.md, docs/local-development.md: mention the `py` variant
and `--script sh|ps|py`.
- docs/reference/integrations.md: add `py` to the `--script` rows for
install/switch/upgrade.
- .devcontainer/devcontainer.json: auto-approve `.specify/scripts/python/`.
Closes#3284.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 298d6ec2-a330-49bc-9394-fe2b77f25ff3
* docs(scripts): address review — accurate paths, prompt behavior, scoped py claims
Addresses the review on PR #3653:
- AGENTS.md: fix the agent-context Python path to its real
`extensions/agent-context/scripts/python/` location.
- AGENTS.md: qualify that only templates that invoke a helper script
carry `scripts:` frontmatter (constitution/specify do not).
- AGENTS.md: narrow the availability claim — `py` covers the core
command templates; the bundled extensions ship Python scripts on disk
but their command templates still invoke shell variants, so `--script
py` does not yet route extension commands to Python.
- AGENTS.md / quickstart / local-development: describe the interactive
prompt vs. non-interactive OS default instead of "auto-selects".
- local-development: add `--script py` to the wrong-script-type fix.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 298d6ec2-a330-49bc-9394-fe2b77f25ff3
* docs(scripts): trim deprecation timeline, correct parity/scope claims (review)
Addresses the second review on PR #3653:
- Remove the speculative four-phase deprecation timeline from AGENTS.md.
A forward-looking removal schedule is roadmap content, not contributor
guidance, and its phases lacked an actionable adoption signal. Replace
it with the concrete contributor parity rule plus a one-line
current-posture note pointing removal work to the #3277 epic.
- Stop stating dual-maintenance as already eliminated: reframe "single
source of truth" as the intended direction, noting all three variants
are still maintained in parallel today.
- Correct the parity-coverage claim: Python ports have output-parity
tests where the contract is stdout-based and unit tests elsewhere,
rather than every file being compared to every shell counterpart.
- Scope the `scripts:` frontmatter rule to core command templates and
note the agent-context/git extension templates don't use it yet.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 298d6ec2-a330-49bc-9394-fe2b77f25ff3
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* chore: bump version to 0.13.4
* chore: begin 0.13.5.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* docs(concepts): document the spec-of-specs feature breakdown approach (#3423)
Add a dedicated "Spec of Specs" concept page describing how to decompose a
large feature into a roadmap of smaller, independently-specified sub-features
using the existing Spec Kit flow. Covers the roadmap pass, the roadmap
artifact template, specifying each sub-feature, bidirectional sub-spec/roadmap
linking, keeping them in sync, a worked example, and optional automation.
Link the new page from the "Handling Complex Features" decomposition section
and add it to the docs table of contents.
Closes#3423
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f236bc38-a7c6-4063-a79c-6ba81aa685b5
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(scripts): git-ext PowerShell emits the '# To persist' SPECIFY_FEATURE hint (parity)
The Git extension's create-new-feature-branch.ps1 printed a non-JSON hint
'SPECIFY_FEATURE environment variable set to: <name>', diverging from every
twin: the bash (create-new-feature-branch.sh) and python
(create_new_feature_branch.py) siblings of the same extension, and the core
create-new-feature.ps1, all emit '# To persist in your shell:
$env:SPECIFY_FEATURE = '<name>''. The old wording is also misleading —
$env:SPECIFY_FEATURE is set only in this child process and never reaches the
agent's shell, so the actionable output is the persist hint. Mirror the core PS
twin's $featureAssignment construction and message.
Test (pwsh CI): the non-JSON output uses the '# To persist in your shell:' form
(fails before: old wording). Verified end-to-end via powershell.exe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(test): fix doubled apostrophe in persist-hint test docstring
Address review: the docstring rendered the documented output form as
'<name>'' (two trailing apostrophes) instead of '<name>'. Docstring-only;
the assertions were already correct.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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): validate cached catalog shape before returning it
The catalog cache-read branch returned json.loads(cache_file) directly, skipping
the shape validation the fresh-fetch branch enforces (dict root + 'integrations'
mapping). A poisoned or older-format cache (e.g. {"integrations": []}) was
therefore returned as-is and later crashed with 'AttributeError: list object has
no attribute items' when the caller iterated integrations. Validate the cached
object the same way; the raised ValueError is already caught by the surrounding
handler, which drops the corrupt cache and refetches from source.
Test: a fresh-but-mis-shaped cache is dropped and the valid source refetched
(fails before: AttributeError).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(integrations): share one catalog-shape validator across cache and fetch
Address review: the cache-read path checked only that the payload was a
dict with a dict 'integrations', while the fresh-fetch path also required
'schema_version'. That asymmetry let an older/poisoned cache such as
{"integrations": {}} (no schema_version) bypass the format contract
instead of being dropped and refetched.
Introduce a shared `_catalog_shape_error()` helper and use it in both
paths so they enforce the same contract (dict + schema_version + dict
integrations). The fresh path still raises IntegrationCatalogError with
the "Invalid catalog format from <url>" prefix; the cache path still
raises ValueError (caught to drop+refetch). Add a test for the
missing-schema_version cache case.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(integrations): unit-test the shared catalog-shape validator directly
Replace the integration-level missing-schema_version cache test (which
was masked by multi-source merging — a sibling catalog source still
supplied the entry, so it passed regardless of the fix) with a direct
unit test of _catalog_shape_error. This deterministically proves both
paths now reject a payload missing schema_version, a non-dict
integrations, or a non-dict payload, and accept a well-formed one.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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(bundler): reject non-list 'catalogs' in bundle-catalogs.yml with a clean error
_merge_config guarded only 'if not catalogs: return', so a non-empty scalar
(catalogs: 5) passed through and raised a raw 'TypeError: int object is not
iterable' from the loop below — escaping the module's BundlerError error
contract. The sibling reader of the same file (commands_impl/catalog_config.py,
used by 'bundle catalog list') already raises an actionable BundlerError for the
identical mis-shape. Add the same isinstance(list) guard so both readers of
bundle-catalogs.yml agree.
Test: 'catalogs: 5' raises BundlerError('...must be a list...') (fails before:
raw TypeError).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(bundler): reject falsy non-list 'catalogs' too (is None, not falsy)
Address review: the isinstance guard sat after `if not catalogs: return`,
so falsy non-list values (`catalogs: false`, `0`, `''`, `{}`) hit the
early return and were silently accepted instead of raising the promised
BundlerError. Only an absent/None value means "nothing to merge".
Change the early return to `if catalogs is None`, mirroring the sibling
reader (commands_impl/catalog_config._read). An empty list stays valid
(the merge loop is a no-op). Add parametrized tests for the falsy
non-list cases and for the absent/empty-list no-op.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A bracketed-but-invalid IPv6 authority (e.g. "https://[not-an-ip]/c.json")
parses cleanly under urlparse() on Python < 3.14 and only raises ValueError
lazily on the first .hostname access. add_source read parsed.hostname
outside the try/except ValueError guard, so on the interpreters spec-kit
supports (>=3.11) that raw ValueError leaked past the CLI's
`except BundlerError`, surfacing an uncaught traceback instead of the clean
"Invalid catalog url" domain error. (The raise moved eager into urlparse()
only in 3.14.)
Read parsed.hostname inside the try and reuse the value for both the
HTTPS/localhost check and the require-host check. This also protects the
later _derive_id() call on the same URL.
Regression tests: a bracketed-non-IP URL, plus a monkeypatched
lazy-.hostname raiser that reproduces the pre-3.14 shape independently of
the running interpreter (fails with a raw ValueError before the fix).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a skills-based integration for the Factory Droid CLI alongside the
existing Claude/Codex skills agents. The integration scaffolds
`.factory/skills/speckit-*` directories and documents the install step
in the devcontainer post-create script via the official npm
distribution (`npm install -g droid`), which matches the layout of every
other CLI install block above and avoids executing an unverified remote
shell installer. The integration is also added to the user-facing
supported-agent table in `docs/reference/integrations.md` so the new key
is discoverable from the published documentation, as required by the
"Updating this documentation" guideline in AGENTS.md.
Operator-supplied extra args via `SPECKIT_INTEGRATION_DROID_EXTRA_ARGS`
are appended after the canonical Spec Kit flags so the canonical flags
are always present in argv. The Factory Droid CLI parser uses
last-wins duplicate-flag semantics (verified empirically against
droid 0.175.0), so a later operator-supplied value may override the
canonical one — this is a deliberate inversion of the cursor-agent /
opencode / codex ordering.
Includes:
- `src/specify_cli/integrations/droid/__init__.py` (subpackage)
- `tests/integrations/test_integration_droid.py` (46 tests, including
regression coverage for the no-trailing-newline frontmatter fusion
bug, idempotent skill injection, and env-var path resolution)
- `integrations/catalog.json` entry + `updated_at` bump
- Alphabetical registration in `src/specify_cli/integrations/__init__.py`
and `tests/integrations/test_registry.py`
- Devcontainer Droid install block via the npm distribution
(`npm install -g droid`), replacing the earlier curl-based installer
- User-facing supported-agent table row in
`docs/reference/integrations.md` (key `droid`, `.factory/skills/`
layout, `/speckit-<command>` invocation)
- `AGENT_CONFIG` entry and matching alphabetical entries in
`tests/test_agent_config_consistency.py` (`ISSUE_TEMPLATE_AGENT_KEYS`)
and the three issue-template dropdowns (`agent_request.yml`,
`bug_report.yml`, `feature_request.yml`) so
`test_issue_template_agent_lists_match_runtime_integrations` keeps
the runtime/template surfaces synchronized
Closes#822
Assisted-by: Droid (oracle-reviewer)
Assisted-by: Droid (model: MiniMax M3, autonomous)
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
* docs(installation): document the 'py' (Python) script type
The installation guide's "Specify Script Type" section only showed
`--script sh` and `--script ps`, and the installed-variant list only
mentioned `.specify/scripts/bash/` and `.specify/scripts/powershell/`.
The CLI has a third script type, `py`: `SCRIPT_TYPE_CHOICES` in
`_agent_config.py` includes `"py": "Python"`, and `shared_infra.py`
installs a `python/` variant directory (plus the platform shell fallback)
when `--script py` is chosen.
Add the missing `--script py` force example and a `.specify/scripts/python/`
bullet so the docs match the code. Docs-only.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(installation): frame Python as a first-class third script type
Address review feedback: update the section heading and intro so Python
is presented as a first-class script type alongside Shell and PowerShell,
matching the added --script py example and python/ variant directory.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The post-init "Next Steps" panel renders recommended slash commands via
the nested `_display_cmd()`. It special-cased dollar-skills agents, kimi,
slash-skills agents, and cline, but not Forge. For a Forge project
`_display_cmd` fell through to `return f"/speckit.{name}"`, printing
`/speckit.constitution`, `/speckit.specify`, etc.
Forge only registers the hyphenated form (`/speckit-<name>`, per
`format_forge_command_name` / `ForgeIntegration.build_command_invocation`,
and the generated command-file tests already assert this), so the panel
told Forge users to run commands that don't exist under the dotted name.
Add `forge_skill_mode` alongside `cline_skill_mode` and include it in the
hyphenated-slash condition, mirroring how cline (also a non-skills
markdown agent with hyphenated commands) is handled. Other agents
unaffected.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Forge is a hyphenated slash-command agent: it registers its commands as
`/speckit-<name>` (see `format_forge_command_name` and
`ForgeIntegration.build_command_invocation`), exactly like Cline.
`HookExecutor._render_hook_invocation` special-cases dollar-skills agents,
kimi, cline, and slash-skills agents, but had no Forge branch. Forge
matches none of those, so it fell through to `return f"/{command_id}"`
and rendered the DOTTED form — `/speckit.plan`, `/speckit.git.commit` —
which Forge does not recognize as a registered command.
Add a Forge branch mirroring the adjacent Cline branch, using
`format_forge_command_name` (idempotent, same contract as the Cline
formatter). Non-Forge agents are unaffected.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`workflow add` gated the local-file branches on a case-SENSITIVE
`.suffix in (".yml", ".yaml")` (the `--dev` branch and the plain
local-path branch), while every other YAML-file detector in the CLI
normalizes case: `workflow run` uses `source_path.suffix.lower()` and
`WorkflowEngine.load_workflow` uses `path.suffix.lower()`.
The result was an add/run inconsistency: `specify workflow run Sample.YAML`
loads the file, but `specify workflow add Sample.YAML` does not recognize
it as a local workflow — the `--dev` branch rejects it with "--dev source
must be a workflow YAML file ..." and the plain path falls through to a
catalog lookup that fails with "not found in catalog".
Add `.lower()` to both suffix reads so `workflow add` matches its siblings.
The lowercase happy path is unchanged.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A workflow list-literal expression with a trailing (or leading/double) comma —
'{{ [1, 2,] }}' — evaluated to [1, 2, None]: _split_top_level_commas returns a
trailing empty segment, which _evaluate_simple_expression resolves as an empty
dot-path to None. That silently widens membership tests and renders a stray
None in joins. Python and Jinja2 both tolerate trailing commas.
Drop whitespace-empty segments from the comprehension. An intentional
empty-string element ('') survives because its segment strips to "''" (truthy),
so ['', 'a'] is preserved. Completes the quoted-comma handling from #3134.
Test: [1, 2,] and [1,, 2] -> [1, 2]; ['', 'a'] -> ['', 'a'] (fails before:
trailing None).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
StepRegistry.add read existing = self.data['steps'].get(step_id, {}) then called
existing.get('installed_at', ...). A corrupted-but-parseable registry holding a
non-dict entry (e.g. {'steps': {'foo': 'corrupted'}}) — which _load() accepts,
since it validates only the top-level dict and that 'steps' is a dict — made
add() raise AttributeError. WorkflowRegistry.add was hardened for exactly this
(#3419); mirror its isinstance guard so a non-dict existing entry is treated as
absent.
Test copies the WorkflowRegistry sibling test for StepRegistry (fails before:
AttributeError on existing.get()).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BundleManifest.from_dict guarded 'requires' and 'provides' with
"must be a mapping when present", but a present-but-non-mapping 'integration'
(e.g. a bare string "copilot") silently failed the isinstance(dict) check and
was dropped — leaving the bundle wrongly integration-agnostic (is_agnostic()
True) instead of surfacing the authoring mistake. Add the same guard so
'integration' is consistent with its sibling mapping fields.
Test: integration='copilot' now raises BundlerError (fails before: silently
dropped, no raise).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_try_dispatch guarded only 'if not integration_key', then called
get_integration(integration_key). A non-string integration (a list/dict, or an
expression like integration: "{{ steps.pick.output.agents }}" that resolves to a
list) reached the registry dict lookup and raised 'TypeError: unhashable type:
list', aborting the entire workflow run. Widen the guard to also require a str,
so a non-string integration is treated as not-dispatchable and execute() falls
through to its existing FAILED StepResult (unconfigured integration=None still
returns None as before). Applied to both command and prompt steps.
Tests: a list integration now yields a FAILED result (fail before: TypeError).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'specify init --script' accepts sh, ps, or py (init.py + SCRIPT_TYPE_CHOICES in
_agent_config.py), and 'specify init --script py' scaffolds Python scripts. The
core.md option reference listed only 'sh|ps', so a user following the canonical
docs never learns about --script py. Update the option row to sh|ps|py.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The interactive gate prompt guarded numeric choices with raw.isdigit(), but
str.isdigit() returns True for characters int() rejects — superscripts/subscripts
like '²'. So typing '²' passed the guard and int('²') raised an uncaught
ValueError, crashing the prompt loop. Use raw.isdecimal(), which is exactly the
decimal-digit set int() accepts (Numeric_Type=Decimal), so such input is treated
as an invalid choice and re-prompted. No behavior change for valid input.
Test: input '²' then '1' returns the first option (fails before: ValueError).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cline installs its slash-commands with hyphenated names (speckit-plan,
speckit-git-commit) via format_cline_command_name + the hyphenated
command_filename, but ClineIntegration inherited MarkdownIntegration's
build_command_invocation, which builds the dotted /speckit.<cmd> — a name Cline
never registered.
Add a build_command_invocation override reusing format_cline_command_name,
producing /speckit-<name>, mirroring the ForgeIntegration fix. Cline was the only
remaining markdown integration with invoke_separator='-' + hyphenated
command_filename that lacked the override.
Tests assert Cline core + extension invocations are hyphenated, incl. args
(fail before: dotted /speckit.plan / /speckit.git.commit).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: bump version to 0.13.3
* chore: begin 0.13.4.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(integrations): exit cleanly on malformed --integration-options quoting
_parse_integration_options called shlex.split(raw_options) unguarded. an
unbalanced quote (e.g. --integration-options='--commands-dir "foo') makes
shlex raise ValueError('No closing quotation'), so a raw traceback escaped
instead of the typer.Exit(1) error every other bad-input path in this function
produces. reachable from specify init and every integration install/switch/
upgrade/migrate that accepts --integration-options.
wrap the split and convert ValueError into the same clean CLI error. added a
regression test; confirmed it fails on the pre-fix code (raw ValueError).
* escape user-controlled values in integration-options error messages
the malformed-quoting handler (and the unexpected/unknown option
branches) interpolate raw_options/token into console.print. a value
carrying an unbalanced rich tag like '--commands-dir "[/red]foo' first
trips the intended shlex ValueError, but the error print then raises
rich.errors.MarkupError and leaks a traceback anyway. escape all three
before printing so the clean typer.Exit survives.
added a regression covering both the shlex path and a bare markup token.
* address review: drop redundant, mis-described markup test
The removed test's docstring claimed the shlex failure branch
interpolates raw_options into console.print; it only prints {exc},
which never contains the caller's markup. Its two assertions also
duplicated test_bad_option_token_with_rich_markup_exits_cleanly (the
'[/red]foo' case) and the shlex-path case already covered by
test_unbalanced_quote_exits_cleanly. The real change here remains the
escape() of the two user-controlled token prints.
* docs: document __SPECKIT_COMMAND_ token for cross-command references
the development guide's 'Body (Markdown)' section listed $ARGUMENTS and
{SCRIPT} but never mentioned __SPECKIT_COMMAND_<NAME>__, the agent-neutral
token that resolve_command_refs() renders into each agent's invocation
syntax. with no signal the token exists, an author naturally hard-codes a
literal like /speckit.my-ext.prepare — correct for one agent, broken on the
rest (the root cause behind #3451).
added it to the placeholder list plus a 'Referencing other commands'
subsection: why a literal isn't portable, the name->token encoding, and a
worked example showing the same token render as /speckit.bug.fix for a
slash agent and /speckit-bug-fix for a skills agent. examples verified
against resolve_command_refs and the first-party bug/git extensions.
phase 1 of the plan in #3474; addresses the discoverability gap for #3451.
* address review: describe separator-based token rendering accurately
Copilot flagged (and mnriem asked me to address) that the section implied
__SPECKIT_COMMAND_<NAME>__ always resolves to each agent's native
invocation, including $speckit-* for Codex/ZCode. the resolver
(resolve_command_refs) only emits /speckit<separator>... based on the
active integration's invoke_separator; the $ and /skill: prefixes come
from later skills-output post-processing, not the token resolver itself.
reworded both the placeholder-list entry and the two explanatory
paragraphs to describe separator-based rendering, and moved the
prefix-in-skills-mode detail to a parenthetical example rather than
stating it as the token's guaranteed output.
* address review: qualify token portability for skills-mode extensions
Copilot correctly noted the __SPECKIT_COMMAND_<NAME>__ token is not yet
resolved for extension-generated skills: _register_extension_skills()
calls resolve_skill_placeholders() and post_process_skill_content() but
never resolve_command_refs(), so the token reaches Codex/ZCode/Kimi
verbatim in skills mode. Token resolution only runs in the command-file
rendering path (CommandRegistrar). Add an explicit limitation note so the
guidance no longer implies universal portability.
The FanOutStep class docstring stated that fan-out execution is
"currently sequential" and that `max_concurrency` is "accepted but not
enforced". That has been inaccurate since #3224, which added a bounded
thread-pool concurrency path to `WorkflowEngine._run_fan_out` that honors
`max_concurrency > 1`.
Update the docstring to match the engine's own `_run_fan_out` docstring:
`max_concurrency <= 1` (the default) runs items sequentially, while `> 1`
runs up to that many items concurrently on a bounded thread pool.
Docstring-only; no behavior change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WorkflowCatalog._fetch_single_catalog and StepCatalog._fetch_single_catalog
opened the catalog URL with open_url(entry.url, timeout=30) and validated
only the final resp.geturl(). open_url follows redirects, so an https://
catalog entry that 30x-redirects through a non-HTTPS host mid-chain could
let a network attacker rewrite the next hop and slip a payload past the
terminal-URL-only check. The payload then drives step/workflow catalog data.
Pass a redirect_validator that runs the existing HTTPS/hostname check before
every redirect hop, keeping the final geturl() check as a defense-in-depth
backstop. This brings both workflow catalog loaders to parity with the
presets (#3523) and extensions (#3524) catalog fetchers.
Tests: add per-hop redirect-validation tests for both WorkflowCatalog and
StepCatalog (a non-HTTPS intermediate hop is rejected); both fail before the
fix ("NoneType object is not callable" — no validator passed). Update the two
existing malformed-redirect tests whose open_url stub lacked the
redirect_validator kwarg.
* Add pipeline extension
Chains the Spec Kit phases into one guided, single-invocation pipeline with a
deterministic phase resolver and one interactive clarify gate.
* refactor(pipeline): replace extension with converge-based workflow
Drop the extensions/pipeline Python resolver, command wrappers, and test
suite in favor of a single workflows/pipeline/workflow.yml, per review
feedback that the workflows engine already owns phase orchestration,
ordering, validation, and resume.
Rework the loop around the new speckit.converge command: analyze runs as a
single pre-implement consistency pass (its findings are artifact-level, not
code-level), and a bounded post-implement convergence loop
(implement -> converge, up to 3 cycles) closes any code/spec gaps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(pipeline): move workflow to external repo, register in community catalog
* chore: bump pipeline catalog entry to v1.1.0
Point the community catalog entry at the corrected v1.1.0 release of
domattioli/spec-kit-workflow-pipeline and raise the advertised minimum
to >=0.11.2 (the release that introduced speckit.converge). Addresses
Copilot review feedback on the catalog metadata.
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>
---------
Co-authored-by: domattioli <domattioli@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(workflows): reject a non-string 'integration'/'model' in command & prompt steps
A non-string `integration` on a command or prompt step is passed to
`get_integration()`, which uses it as a dict key: an unhashable list/dict
raises a raw `TypeError` there — and because neither `validate()` nor
`validate_workflow` checked the type, this crashes even a *validated* run,
not just an unvalidated one. A non-string `model` likewise reaches
`build_exec_args()` and is fed into the CLI argv.
Guard both fields in `validate()` (reject a literal non-string, mirroring the
existing 'command'/'prompt'/'input'/'options' checks) and in `execute()`
(fail the step cleanly rather than take down the whole run, mirroring the
'input'/'options' guards). An explicit YAML-null (inherit the workflow
default) and a "{{ ... }}" expression both stay valid.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): route falsey non-string integration/model to the type guard
Address Copilot review: `config.get("integration") or context.default_integration`
(and the model equivalent) coerced a *falsey* non-string ([], {}, 0, False) into
the workflow default before the type guard ran. On an unvalidated execute() such a
step was silently accepted and — with a configured default — could dispatch using
the wrong integration/model instead of failing with the contract error.
Fall back to the workflow default only for genuinely-unset values (missing /
YAML-null / empty string) so every non-string reaches the guard. Add parametrized
falsey execute() cases ([], {}, 0, False) to both TestCommandStep and
TestPromptStep; with the fix stashed all 8 fail (swallowed into the default).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A workflow input whose `enum` is a scalar or string (e.g. `enum: 5`,
`enum: "abc"`) previously slipped past `validate_workflow` and crashed
at run time. The `value not in enum_values` membership test in
`_coerce_input` raises a raw `TypeError` ("argument of type 'int' is
not iterable") for a scalar, and a bare string turns enum membership
into a silent substring test. The `TypeError` also escapes
`validate_workflow`'s `except ValueError`, breaking its documented
"return a list of errors, never raise" contract.
This is the same unvalidated-`execute()` crash class as the fan-in
`wait_for` (#3482) and fan-out step-template (#3537) fixes: `validate()`
should reject the value, but the value can still reach the engine via
`execute()`, which accepts unvalidated definitions.
Fix:
- `_coerce_input` requires a list `enum` (or `None`), raising a clean
ValueError for any other shape — so both `validate_workflow` and
runtime `_resolve_inputs` fail fast with a clear message.
- `validate_workflow` checks `enum` shape directly (not only via the
default-coercion path, which is reached only when a `default` exists),
and strips a malformed `enum` before coercing the default so the
wrong-typed-default error is not duplicated as an enum-shape error.
- The `integration: auto` sentinel only strips a *list* `enum`; a
non-list `enum` stays in the definition so it is rejected rather than
silently exempted by the `auto` membership skip.
Tests cover all three layers: `_coerce_input` directly, authoring-time
`validate_workflow` (with no default present), and runtime
`_resolve_inputs`, plus the `integration: auto` interaction.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: bump version to 0.13.2
* chore: begin 0.13.3.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
`CommandStep.validate` only checked that a `command` field is *present*,
never its type. On an unvalidated run (the engine does not auto-validate
before `execute`) a non-string `command` — null, a list, an int — was
passed straight through `_try_dispatch` to the integration's
`build_command_invocation`, which does `command_name.startswith("speckit.")`
and crashes the whole workflow with a raw `AttributeError` once a
resolvable integration with an installed CLI is found.
Guard both paths, mirroring the sibling steps:
- `validate()` rejects a non-string `command` (like prompt-step `prompt`
#3582 and shell-step `run`).
- `execute()` fails the step cleanly with the same contract error before
dispatch (like the existing `input`/`options` guards in this file), so
an unvalidated run FAILs the step instead of crashing the run.
An expression like `{{ inputs.cmd }}` is still a string, so it stays valid.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`GateStep.validate` rejects a non-list (or empty) `options` and requires
every option to be a string, but the engine does not auto-validate before
`execute`. On an unvalidated run a scalar/dict/None `options` reached
`_prompt` and crashed the whole workflow with a raw `TypeError`
(`enumerate`/`len` on a non-iterable) or `KeyError` (indexing a dict); an
empty list spun `_prompt`'s input loop forever; a non-string option crashed
the reject check at `choice.lower()` with `AttributeError`.
Guard `execute` to FAIL the step cleanly instead, before the non-TTY
PAUSE short-circuit so the error surfaces in CI too rather than pausing
and only crashing later on interactive resume. Mirrors the switch 'cases'
and command 'input' unvalidated-execute guards.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(extensions): re-validate catalog URL after redirects (HTTPS parity)
ExtensionCatalog._fetch_single_catalog opened the catalog URL and trusted the
payload without re-validating response.geturl() after redirects. _open_url
follows redirects (stripping auth only on an HTTPS->HTTP downgrade), so an
https:// catalog entry that 30x-redirects to http://attacker/... was still
fetched and trusted. The payload supplies each extension's download_url +
sha256, so a redirected payload can drive install of an arbitrary archive that
passes sha256 verification.
Add the post-redirect geturl() re-validation via _validate_catalog_url,
mirroring integrations/catalog.py, presets, workflows/catalog.py, and bundler
adapters. Sibling of the same fix in the presets catalog fetcher.
Test: an HTTPS URL whose response.geturl() reports http:// is rejected
(ExtensionError). Completed existing fetch-test mocks that predated this
behavior to report geturl() like a real urllib response.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(extensions): correct auth-strip comment; validate every redirect hop + guard legacy fetch_catalog
- Correct the comment: _StripAuthOnRedirect strips auth not only on an
HTTPS->HTTP downgrade but also whenever the redirect leaves the configured
trusted hosts. The comment now describes both cases.
- Parity with the presets fix: validate EVERY redirect hop (not just the
terminal URL) so an https -> http -> attacker-https chain can't slip a
redirected payload past the final-URL check. _open_url forwards a
redirect_validator to open_url; _fetch_single_catalog passes
_validate_catalog_url through it while keeping the final geturl() check.
- Give the legacy public fetch_catalog() single-catalog path the same
redirect_validator + final geturl() validation (it previously parsed the body
with no redirect check).
Tests: an intermediate http hop is rejected, and the legacy fetch_catalog()
rejects an HTTPS->http redirected payload (both fail before). Full
test_extensions.py (356) green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(extensions): cover legacy fetch_catalog() per-hop redirect validation
The legacy fetch_catalog() regression test only exercised the terminal geturl()
check, so it would still pass if the per-hop redirect_validator were dropped from
that duplicated path. Add test_fetch_catalog_legacy_validates_every_redirect_hop,
which asserts fetch_catalog() supplies a redirect_validator that rejects an
insecure intermediate hop (fails before: the legacy path passed no validator ->
NoneType not callable).
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): re-validate catalog URL after redirects (HTTPS parity)
PresetCatalog._fetch_single_catalog opened the catalog URL and trusted the
payload without re-validating response.geturl() after redirects. _open_url
follows redirects (stripping auth only on an HTTPS->HTTP downgrade), so an
https:// catalog entry that 30x-redirects to http://attacker/... was still
fetched and trusted. The catalog payload supplies each preset's download_url +
sha256, so a redirected payload can drive install of an arbitrary archive that
passes verify_archive_sha256.
Add the post-redirect geturl() re-validation via _validate_catalog_url,
mirroring integrations/catalog.py, workflows/catalog.py, and bundler adapters —
and presets/_commands.py, which already does this on its --from download path.
This is the lone preset catalog-fetch site missing the guard.
Test: an HTTPS URL whose response.geturl() reports http:// is rejected
(PresetValidationError). Completed four existing fetch-test mocks that predated
this behavior to report geturl() like a real urllib response.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(presets): validate every redirect hop + guard the legacy fetch_catalog path
Two follow-ups to the catalog redirect hardening:
1. Validate every redirect hop, not just the terminal URL. A final-geturl-only
check passes an https -> http -> attacker-controlled-https chain: the insecure
intermediate hop lets a network attacker rewrite the next redirect. _open_url
now forwards a redirect_validator to open_url (called before each hop), and
_fetch_single_catalog passes _validate_catalog_url through it while retaining
the final geturl() check — mirroring bundler/services/adapters.py.
2. The legacy public fetch_catalog() single-catalog path parsed response.read()
with no redirect check at all. Give it the same redirect_validator + final
geturl() validation.
Tests: a stubbed intermediate http hop is rejected (redirect_validator), and the
legacy fetch_catalog() rejects an HTTPS->http redirected payload (fail before:
no raise). Existing fetch-test mocks updated to accept the redirect_validator
kwarg and report geturl() like a real response. Full test_presets.py (365) green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(presets): drop duplicate geturl mock; add legacy fetch_catalog per-hop test
- Remove the duplicate mock_response.geturl.return_value assignment left by the
geturl mock-completion pass (the explanatory comment was stranded between the
two identical assignments); keep a single assignment after the comment.
- Add test_fetch_catalog_legacy_validates_every_redirect_hop so the legacy
fetch_catalog() path is verified to supply the redirect_validator (rejecting an
insecure intermediate hop), not just the terminal geturl() — parity with
_fetch_single_catalog and the #3524 sibling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(scripts): port create-new-feature, setup-plan and setup-tasks to Python
Ports the three core workflow scripts to Python as part of #3280,
following the check-prerequisites PoC pattern from #3302. Adds
resolve_template() to the shared common.py module and parity tests
that run bash and Python side by side.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(tests): treat only None env as unset in parity run helper
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): fall back to directory scan on any registry error, skip hidden preset dirs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(templates): add py: lines for setup_plan and setup_tasks
Ships with the scripts they reference; the remaining templates got
their py: lines in #3403.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: support py variant in skills placeholder resolver
resolve_skill_placeholders only accepted sh/ps, so a py init option
fell into the fallback path and {SCRIPT} rendered without an
interpreter prefix. Accept py and prefix the resolved interpreter,
matching process_template. Also guard ps_cmd against a missing
PowerShell with a clear assert.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: pin clean-error behavior for invalid --number
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(scripts): reword unused-arg comment to match implementation
The loop accepts and silently ignores extra positional args (it doesn't
build a collected list); match the wording to what the code and
setup-plan.sh actually do.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: fall back when configured script variant is missing from frontmatter
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): reject signed/whitespace --number values to match bash 10# parity
The bash twin uses $((10#$BRANCH_NUMBER)), which rejects signed and
whitespace-padded values. Python's int() accepted them (e.g. -1),
producing a malformed -01-... prefix that sequential scans ignore.
Restrict --number to unsigned decimal digits before conversion, and
pin the parity with a bash-comparison test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): complete Python port installation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(integrations): fall back for missing script variants
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: make Python script checks platform-aware
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix Windows Python command invocation parity
Use PowerShell's call operator for spaced Python interpreter paths and align setup-tasks missing-template errors across script variants.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): preserve cross-platform Python parity
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: reject signed PowerShell feature numbers
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): align feature number range
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): reject exhausted feature numbers
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): complete create feature parity
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): align create feature outputs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): harden cross-platform parity
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): keep truncation JSON clean
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): align setup failure parity
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): close parity edge cases
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): propagate PowerShell setup errors
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): harden fallback resolution
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): stabilize PowerShell fallbacks
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): complete setup-plan parity
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(cli): require runnable script fallbacks
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(cli): preserve shell fallback without preference
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): restore help and symlink parity
- setup-tasks.ps1: check -Help before unknown-argument validation so
'-Help --bogus' exits 0 like the Bash/Python variants
- common.py: strip the repo root prefix lexically in persist_feature_json
instead of resolve(), so a symlinked specs/ still persists the relative
'specs/NNN-name' path the Bash/PowerShell helpers store
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): align persist-hint quoting with shlex.quote
- create-new-feature.sh: replace printf %q with a shell_quote helper that
emits shlex.quote-identical output, so the persistence hints stay
byte-identical between the Bash and Python variants (printf %q output
also varies between bash versions)
- promote the negative --number test to an all-variants parity test now
that Bash and PowerShell reject signed values consistently
- add a spaced-repo-path parity test for the persistence hints
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CommandRegistrar.parse_frontmatter located the closing delimiter with
content.find("---", 3), a raw substring search. It stopped at the first
"---" anywhere after the opening — including one embedded in a
frontmatter value (e.g. a description "Separate sections with ---
markers") or inside an indented literal block — which truncated the
frontmatter and spilled the remainder into the body, silently corrupting
both the parsed metadata and the rendered command body.
Match the closing "---" on line boundaries, mirroring the line-anchored
scan already used by VibeIntegration._inject_frontmatter_flag.
* Fix reinstall-overwrites-kept-config: preserve config on plain reinstall after --keep-config
Apply the remediation from the bug assessment on issue #3427.
Before the unconditional shutil.rmtree(dest_dir), scan dest_dir for any
*-config.yml and *-config.local.yml files and hold their contents in memory.
After shutil.copytree succeeds, write them back so user-customized values
always win over the packaged defaults.
This mirrors the existing backup/restore logic for the --force reinstall path
but handles the case where remove --keep-config left config files behind in
an unregistered extension directory.
Refs #3427
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: restore method decl, move config restore before registration, preserve file mode
- Restore missing `test_install_force_without_existing` method declaration in
tests/test_extensions.py so pytest collects it as a separate test.
- Move stranded-config restoration to immediately after `copytree`, before
command/skill/hook registration, so a failed registration step can't leave
preserved configs permanently lost.
- Store `(bytes, mode)` tuples instead of bare bytes when rescuing stranded
configs, and reapply the original file mode after writing so permission bits
(e.g. 0600 for credential files) are faithfully restored.
Assisted-by: GitHub Copilot (model: claude-sonnet-4.5, autonomous)
* fix: mask setuid/setgid bits when restoring stranded config file mode
Only preserve user/group read-write bits (mode & 0o660) to avoid
restoring setuid, setgid, or world-writable permissions from a
user-modified config file.
Assisted-by: GitHub Copilot (model: claude-sonnet-4.5, autonomous)
* fix: add copytree rollback path and strengthen regression test with packaged default config
- Wrap shutil.copytree in a try/except BaseException so stranded configs
rescued before rmtree are written back even if copytree fails mid-way
(addresses review comment: configs were permanently lost on copy failure)
- Add a packaged default config to extension_dir in the regression test so
a naive 'restore only when absent' implementation would fail; assert the
user's customized values beat the packaged defaults after reinstall
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
* fix: restore configs with secure atomic writes
Assisted-by: GitHub Copilot (model: gpt-5, autonomous)
* fix: write secure temp file then chmod to preserved_mode; add copytree-failure test
- _restore_stranded_config_file: write content while temp file is at its
secure OS-default mode (typically 0600 on POSIX), then apply the
original preserved_mode after the file is fully written and before the
atomic os.replace. Removes the & 0o660 mask that was silently stripping
world-read and executable bits (e.g. 0644 → 0640).
- Add test_copytree_failure_restores_stranded_config: patches
shutil.copytree to create a partial destination then raise OSError,
then asserts that the preserved config bytes and file mode are restored
by the rollback path and that the extension remains unregistered.
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
* Potential fix for pull request finding 'Unused local variable'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* Potential fix for pull request finding 'Module is imported with 'import' and 'import from''
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* fix: durable staging for stranded configs and import style fix
- Stage stranded config files to a durable rescue_staging_dir
(extensions_dir/.rescue-staging-<id>) before rmtree so original bytes
survive partial rmtree, copytree failure, or partial restore on retry.
On retry the staging dir is detected and its content reused instead of
whatever mix of packaged defaults and partial restores remains on disk.
The staging dir is cleaned up only after every restore succeeds.
- Fix CodeQL: change `import specify_cli.extensions as _ext_module` to
`from specify_cli import extensions as _ext_module` in test file.
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
* fix: harden rescue staging dir - symlink checks, secure writes, cleanup errors
- Thread 14: Change except BaseException to except Exception in the staging
fallback block so KeyboardInterrupt/SystemExit propagate correctly
- Thread 15: Add explanatory comment to the bare pass in the chmod except
block to satisfy static analysis
- Thread 16: Reject a symlinked staging directory and only reload
non-symlinked files whose names match the two recognised config suffixes
- Thread 17: Create each staging file via os.open with mode 0600 and
O_CREAT|O_EXCL before writing so preserved bytes are never transiently
exposed to other local users
- Thread 18: Remove ignore_errors=True from the final staging-dir cleanup
so a failed rmtree propagates rather than silently leaving a stale
backup that could be misread on the next retry
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
* fix: mask file-type bits from chmod, harden staging dir symlink check
- Add `import stat` to imports
- Use `stat.S_IMODE(mode)` before chmod in staging write (thread 20, line 1464)
- Use `stat.S_IMODE(preserved_mode)` and make chmod best-effort in
`_restore_stranded_config_file` (thread 18, line 1492)
- Add `not rescue_staging_dir.is_symlink()` guard to cleanup (thread 19, line 1522)
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
* fix: use completion marker for rescue staging, abort on staging failure, full os.write
Assisted-by: GitHub Copilot (model: GPT-5.3-Codex, autonomous)
* fix(extensions): make rescue staging durable
Assisted-by: GitHub Copilot (model: MAI-Code-1-Flash, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* test(extensions): fix flaky copytree regression test
Assisted-by: GitHub Copilot (model: MAI-Code-1-Flash, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* test(extensions): fix module import alias for review feedback
Assisted-by: GitHub Copilot (model: MAI-Code-1-Flash, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(workflows): keep cleanup warnings single-line and remove dead helper
Assisted-by: GitHub Copilot (model: MAI-Code-1-Flash, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Potential fix for pull request finding 'Module is imported with 'import' and 'import from''
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* Preserve rescued extension config across retry
Assisted-by: GitHub Copilot (model: MAI-Code-1-Flash, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Clarify ignored directory fsync cleanup errors
Assisted-by: GitHub Copilot (model: MAI-Code-1-Flash, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Fix reinstall durability and workflow cleanup warnings
Assisted-by: GitHub Copilot (model: MAI-Code-1-Flash, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Open rescue staging file in binary mode to fix Windows CRLF corruption
On Windows os.open() defaults to text mode, so os.write() of preserved
config bytes containing \r\n was translated to \r\r\n, corrupting the
staged backup and failing the retry-restore regression test. Add
O_BINARY (0 on POSIX) to the staging file open flags.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Load .extensionignore before deleting dest_dir on reinstall
The .extensionignore loader can raise ValidationError (invalid UTF-8) or
OSError. Previously it ran after dest_dir was removed, so such a failure
left the kept config only in the hidden staging directory rather than its
documented location. Load/validate it before the rmtree so every
post-deletion failure path restores the config. Adds a regression test.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Validate .extensionignore before publishing rescue staging
Loading .extensionignore after the rescue staging directory was published
meant a validation failure left a complete staging copy behind. A later
retry (after the user fixed the ignore file and edited the kept config)
would reload the stale staged bytes and silently overwrite the newer
config. Move the loader ahead of reading/creating rescue staging so a
failure aborts while the kept config is still authoritative on disk, and
extend the regression test to prove no staging is published and a retry
adopts the newer bytes.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Harden preserved-config rescue against divergence and long names
Address three review findings on the reinstall config-rescue path:
- A complete .rescue-complete marker proves only that staging finished,
not that dest_dir was modified. A crash after staging sync but before
the rmtree leaves the live kept config intact; if the user edits it
before retrying, preferring the staged bytes silently overwrote the
newer config. The two copies are indistinguishable in provenance from
disk, so detect divergence between a complete staging copy and the live
config and abort (preserving both) instead of unconditionally choosing
staging.
- The staging directory embedded the full extension ID in one path
component. Extension IDs are length-unbounded, so a valid long ID could
install at dest_dir yet fail every reinstall-after-keep-config with
ENAMETOOLONG. Derive the staging component from a fixed-length hash via
a new _rescue_staging_dir() helper.
- The stranded-config restore used the full config filename as a
NamedTemporaryFile prefix; a name already near the component limit plus
the random suffix raised ENAMETOOLONG. Use a short fixed prefix.
Updates the retry regression test to the new divergence semantics and
adds conflict-abort, long-ID, and fixed-prefix coverage.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Harden preserved-config rescue divergence check and fix test path
Assisted-by: GitHub Copilot (model: GPT-5.6, autonomous)
* fix: reject/flag symlinked preserved configs on reinstall
Assisted-by: GitHub Copilot (model: GPT-5.6-Sol, autonomous)
* fix: include symlinks in live-dir config enumeration and address review feedback
- _recognized_config_names() now accepts follow_symlinks=False for live dir
so symlinked *-config.yml entries are detected and treated as conflicts
rather than being silently deleted by rmtree.
- Add explanatory comment to bare 'except OSError: pass' in
_restore_stranded_config_file's finally block.
- Resolve CodeQL dual-import style: use 'from specify_cli import extensions
as _ext_module' instead of 'import specify_cli.extensions as _ext_module'.
Assisted-by: GitHub Copilot (model: claude-sonnet-4, autonomous)
* test: add staging-failure fault-injection test for rescue staging block
Add test_staging_failure_aborts_before_dest_dir_removal covering three
failure modes (mkdir, os.open/O_CREAT, fsync with EIO) in the rescue
staging block. Each parametrized case verifies:
- the install aborts before dest_dir is removed
- the preserved config bytes remain authoritative
- any partial staging is cleaned up and not left as complete
- the extension stays unregistered
Addresses review feedback on PRRT_kwDOPiFCnc6R351t.
Assisted-by: GitHub Copilot (model: claude-sonnet-4.5, autonomous)
* test: add test_retry_restores_config_from_staging_when_live_absent
Exercises the retry-from-staging branch (if staging_is_complete at
line 1505 of extensions/__init__.py) in a scenario where the live
config is absent — simulating a power loss that interrupted the
rollback before it could write the config back.
When the live copy is gone, the live-dir fallback (elif dest_dir.exists())
finds no stranded configs and the packaged default would be kept. Only the
staging-complete branch can restore the original bytes and mode. This proves
staging (not the fallback) is used on retry.
Addresses review feedback on PRRT_kwDOPiFCnc6SAL3L.
Assisted-by: GitHub Copilot (model: claude-sonnet-4.5, autonomous)
* fix: keep staging files writable; record modes in .rescue-modes.json; fix live-only conflict message
Thread 64: Remove os.fchmod/chmod from staged files to avoid Windows
read-only attribute that prevents shutil.rmtree from cleaning up.
Original permission bits are now written to a .rescue-modes.json sidecar
in the staging dir and reloaded during retry, with a fall-back to the
staged file's own mode for backwards-compat with pre-sidecar staging dirs.
Thread 65: Split the ValidationError message for staging-vs-live conflicts
into two accurate cases: files that diverged between both locations
("Both copies have been preserved") and live-only files that have no
backup counterpart, which previously incorrectly claimed "Both copies
have been preserved" and offered a restore instruction that was impossible.
Assisted-by: GitHub Copilot (model: claude-sonnet-4.5, autonomous)
* Potential fix for pull request finding 'Empty except'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* fix: add .keep-config provenance marker to guard rescue path against partially-failed installs
When `remove --keep-config` strands config files, write a `.keep-config`
marker into the extension directory. `install_from_directory` now only
enters the rescue path when that marker is present, preventing a partially-
failed install (which also leaves dest_dir with no registry entry but no
marker) from having its packaged default configs treated as user-preserved
data on a retry from an updated package.
Refs: https://github.com/github/spec-kit/pull/3449#discussion_r3606283457
Assisted-by: GitHub Copilot (model: claude-sonnet-4.5, autonomous)
* refactor: extract _has_keep_config_marker helper and document empty-content choice
Assisted-by: GitHub Copilot (model: claude-sonnet-4.5, autonomous)
* fix: defer rescue-backup cleanup until registry commit; validate modes sidecar shape
Assisted-by: GitHub Copilot (model: GPT-5.6, autonomous)
* fix legacy keep-config rescue and retry baseline handling
---------
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>
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* feat: update Bob integration to skills-based layout for Bob 2.0
Bob 2.0 replaces the command-based workflow (.bob/commands/*.md) with
a skills-based layout (.bob/skills/speckit-<name>/SKILL.md), matching
the pattern used by Claude Code, Codex, and other skills-first agents.
- Switch BobIntegration from MarkdownIntegration to SkillsIntegration
- Update folder/dir from .bob/commands to .bob/skills
- Change extension from .md to /SKILL.md (skills layout)
- Add --skills option (default: True) consistent with Codex pattern
- Update tests to inherit from SkillsIntegrationTests (28 tests pass)
- Bump catalog entry to version 2.0.0 with updated description
Assisted-by: IBM Bob (model: claude-sonnet-4-5, autonomous)
* PR comments fix: keep old Bob 1 commands till next release
* Copilot suggested change
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* feat(bob): address copilot comments, make skills layout default, demote legacy commands to opt-in
* fix(bob): honor legacy_commands in ai_skills persistence and add bob to ALWAYS_SLASH_AGENTS
- init.py: suppress ai_skills=True when --legacy-commands is passed so
extensions and presets target .bob/commands, not .bob/skills
- _invocation_style.py: add 'bob' to ALWAYS_SLASH_AGENTS so init next-steps
and hook invocations always show /speckit-<name> (skills is the default
layout; no ai_skills flag required)
* Copilot suggestion
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(bob): extend IntegrationBase directly to avoid false isinstance(SkillsIntegration)
- bob/__init__.py: switch BobIntegration base from SkillsIntegration to
IntegrationBase; add _BobSkillsHelper for skills-mode delegation; set
invoke_separator='-' explicitly; set _skills_mode flag in setup() so
consumers can derive the effective mode without isinstance checks
- _helpers.py: replace isinstance(integration, SkillsIntegration) guard
with getattr(_skills_mode) so legacy-commands mode does not persist
ai_skills=True
- _invocation_style.py: remove 'bob' from ALWAYS_SLASH_AGENTS — Bob 2.0
skills are invoked via natural language, not /skill-name slash commands
- integrations/catalog.json: advance updated_at to 2026-07-15
* fix(lint): remove unused SkillsIntegration import from _helpers.py
* Copilot suggested change
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* feat(bob): add bob skills integration with registrar-based mode detection
* address 3 comments from copilot
* feat(bob): update registrar config to use legacy commands layout
* fix lint
* Suggested fix from Copilot
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix pr comment
* fix pr comment
* fix pr comment
* refactor(bob): resolve skills mode via base-class hooks + fix command-ref separators
Rework the dual-mode handling introduced for Bob 2.0 so an integration's
internal representation never leaks into shared init/install/upgrade code,
and fix the legacy command-reference separator surfaced in review.
Base-class contract:
- Add IntegrationBase.is_skills_mode(parsed_options) — the single hook the
shared machinery consults to decide whether to persist ai_skills and render
skill invocations. SkillsIntegration returns True; Copilot honors --skills /
self._skills_mode; Bob returns `not legacy_commands`.
- Add IntegrationBase.invoke_separator_for_mode(skills_enabled) — resolves the
command-ref separator from a project's persisted mode for registration paths
that only have the ai_skills flag (no CLI parsed_options). Default is
behavior-preserving; Bob maps skills->"-", legacy->".".
- BobIntegration stays on IntegrationBase (mirroring Copilot, the other
dual-mode agent) and delegates setup() to internal _BobSkillsHelper /
_BobMarkdownHelper. Removes the _skills_mode method and all
isinstance(SkillsIntegration) / callable(_skills_mode) probing from
_helpers.py and init.py.
Fix legacy separator (review feedback): CommandRegistrar.register_commands and
PresetManager._resolve_skill_command_refs previously read the single static
AGENT_CONFIGS[key]["invoke_separator"], so legacy .bob/commands/ extension and
preset command refs rendered /speckit-<cmd> instead of Bob 1.x /speckit.<cmd>.
Both now resolve the separator per project mode via invoke_separator_for_mode.
Tests: add regression coverage for the is_skills_mode / invoke_separator_for_mode
hooks and legacy extension command-ref separators; normalize a width-sensitive
workflow assertion to match its siblings. Full suite green.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf
* fix(bob,copilot): address review — preserve legacy layout, dual-mode separators, extension-skill token resolution
Addresses PR review 4716036212 (3 comments):
1. Bob legacy-install regression: `use`/`switch`/`upgrade` on an existing
Bob 1.x project (only `.bob/commands/` on disk, no stored
`legacy_commands`) called `is_skills_mode(None)` -> True and rewrote
`ai_skills=True`, silently switching extension/command-reference handling
to the skills layout. `is_skills_mode` now takes an optional `project_root`;
Bob preserves an already-installed legacy layout until an explicit upgrade
creates `.bob/skills/`. A fresh project still defaults to skills.
2. Copilot dual-mode separator: `invoke_separator_for_mode` was inherited
from the base (mode-independent) and returned Copilot's static `.`, so
preset/extension command refs in a Copilot skills project rendered
`/speckit.<name>` instead of `/speckit-<name>`. Override it on Copilot to
track the persisted `ai_skills` state, consistent with
`build_command_invocation` and `effective_invoke_separator`.
3. Bob extension-skill command-ref tokens: verified that merging main's
generic `_resolve_command_ref_tokens` (#3544) resolves Bob's tokens via
the `CONDITIONAL_SLASH_AGENTS` path (`/speckit-<name>`); added Bob to the
command-ref regression parametrize plus dedicated Bob use-path tests.
All tests pass (full suite green; merged with current main incl. #3544).
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf
* fix(bob): resolve command-ref separator with project-aware mode before shared-infra refresh (review #3415)
The `use`/`switch` paths refresh shared infrastructure via
`_with_integration_setting()` / `_invoke_separator_for_integration()`,
which previously resolved the invoke separator through
`effective_invoke_separator` / `is_skills_mode` WITHOUT a project_root.
For a pre-PR Bob 1.x project (.bob/commands/ on disk, no stored options),
this defaulted to the skills "-" separator and rewrote rendered
shared-template command refs to /speckit-*, even though ai_skills stayed
false. Thread project_root through effective_invoke_separator, the two
runtime helpers, and every call site so Bob's on-disk legacy detection
governs the separator before shared infra is refreshed.
Add a rendered-shared-template regression test covering `use --force`.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf
* fix(bob): scope persisted ai_skills flag to active agent when resolving command-ref separator (review #3415)
`register_commands` runs once per detected agent, but the persisted
`ai_skills` flag describes only the active integration (`opts["ai"]`).
When another agent (e.g. Copilot) is active in skills mode while a
legacy `.bob/commands` layout is also present, the previous code passed
that global `True` to Bob's `invoke_separator_for_mode`, rewriting Bob
1.x command refs to `/speckit-*` instead of `/speckit.*`.
Only consult the persisted flag for the agent it describes
(`opts["ai"] == agent_name`); otherwise resolve the separator from the
agent's own project-aware `effective_invoke_separator(None, project_root)`.
Add regression tests covering the mismatched-active-agent case and a
control for Bob-active skills mode.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf
* fix(bob): detect Spec Kit layout from managed artifacts, not any skills dir (review #3415)
Two related mis-detections from review 4723246468:
1. `BobIntegration.is_skills_mode` treated the mere presence of a
`.bob/skills/` directory as proof the project is skills-based. A legacy
Spec Kit install (managed `.bob/commands/speckit.*.md`) that also carried
unrelated Bob 2 skills would be misclassified as skills, so
`integration use bob` persisted `ai_skills` and rewrote shared refs.
Now the layout is inferred from managed Spec Kit artifacts: legacy/command
mode only when managed `speckit.*.md` command files exist and no managed
`speckit-*` skill dirs do.
2. The `register_commands` separator for an inactive agent used a disk-based
`effective_invoke_separator(None, project_root)` fallback that could pick
the skills separator even though the registrar writes the static command
layout (`.bob/commands/*.md`). Inactive agents now resolve the separator
from the registrar's actual output layout (`extension == "/SKILL.md"`),
so command-layout files keep `/speckit.*` refs regardless of sibling dirs.
Update the affected hook/E2E tests to use managed artifacts and add
regression tests for the mixed-layout and inactive-registrar scenarios.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf
* fix(bob): apply managed-artifact detection on upgrade + consistent skill post-processing (review #3415)
Two issues from review 4723782860:
1. `BobIntegration.setup()` resolved the layout via `is_skills_mode(parsed_options)`
WITHOUT `project_root`, so `integration upgrade bob` on a Bob 1.x install
(managed `.bob/commands/speckit.*.md`, no stored options) ignored the
existing command files, generated skills, and stale-deleted the legacy
commands — silently migrating the project. Pass `project_root` so the same
managed-artifact detection used by `use` also governs upgrades.
2. Only `_BobSkillsHelper` overrode `post_process_skill_content` to suppress
the shared slash-command hook note. Preset/extension skill generators call
that hook on the registered `BobIntegration`, which inherited
`IntegrationBase`'s note-injecting default. Repeat the no-op (delegating to
the skills helper) on the registered class so every Bob skill-generation
path is consistent with intent-activated core Bob skills.
Add regression tests for the upgrade-preservation and post-processing paths.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf
* feat(bob): add --skills migration opt-in; fix separator + manifest loss (review #3415)
Address review #3415 (4724160183):
- Comment 1: Add an explicit `--skills` opt-in to BobIntegration. It forces
the skills layout over on-disk auto-detection, giving legacy Bob 1.x
installs a supported migration path
(`integration upgrade bob --integration-options="--skills"`). `--skills`
and `--legacy-commands` are mutually exclusive (clean exit-1 error).
- Comment 2: In CommandRegistrar.register_commands, derive the command-ref
separator from the output layout (agent_config["extension"]) for the
active agent too, not the persisted ai_skills flag. A command-layout file
(.bob/commands/*.md, .github/agents/*.agent.md) always renders /speckit.*;
only a /SKILL.md scaffold uses /speckit-*. Dual-layout agents (Bob,
Copilot) write skills via their own setup()/skills path, so
register_commands only ever emits their command-layout files.
- Comment 3: Update docs/reference/integrations.md Bob entry to document the
skills-based default (.bob/skills/), the deprecated --legacy-commands
opt-out, and the --skills migration path.
Also fix a latent manifest-loss bug surfaced by the migration path: the
upgrade Phase 2 stale-file cleanup built a throwaway manifest sharing the
integration key and called uninstall(), which always deleted
{key}.manifest.json. Any layout-shrinking upgrade (e.g. legacy->skills)
thus wiped the freshly-saved manifest, leaving the project untracked and
un-upgradeable. uninstall() now takes remove_manifest (default True); the
stale-cleanup pass passes False.
Adds regression tests for the --skills opt-in, mutual exclusion, corrected
active-agent separator, remove_manifest=False, and an end-to-end
legacy->skills migration that verifies the manifest survives and the
project remains upgradeable. Full suite: 4555 passed, 5 skipped.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf
* docs(agents): align token-resolution comment with output-layout separator rule (review #3415)
Address review #3415 (4725516805). The comment above resolve_command_refs
still described the removed state-based behavior ("resolve it from the
integration using the project's persisted skills state"). Update it to
describe the output-layout rule that register_commands now uses: _sep is
derived from the layout this registrar writes (a /SKILL.md scaffold uses the
skills separator; a command-layout file uses the command separator), not the
persisted ai_skills state. Comment-only change; no behavior change.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf
* fix(bob): reconcile extension artifacts on layout change (review #3415)
When a dual-mode agent (Bob) flips between the legacy commands layout and
the skills layout during `integration upgrade` (via `--skills` /
`--legacy-commands`), the old layout's extension command/skill files were
left orphaned: Phase 2 stale cleanup only removes files tracked by the
*integration* manifest, while extension artifacts are tracked in the
extension registry. Detect the layout flip by comparing whether the old vs
new manifest tracks a `/SKILL.md` scaffold, and when it changed, unregister
the agent's extension artifacts before the existing re-registration so they
are recreated in the new layout (and the per-agent registry is updated).
Preset artifacts are documented as a known, pre-existing cross-cutting gap:
no agent-scoped preset re-registration exists in use/switch/upgrade for any
agent, so reconciling them is out of scope for this Bob migration.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf
* fix(bob): reject layout migration when preset overrides are installed (review #3415)
A command↔skills layout change during `integration upgrade` cannot reconcile
preset artifacts: presets track their command/skill files in per-preset
`registered_commands`/`registered_skills` metadata, and there is no
agent-scoped preset re-registration anywhere in the CLI. Migrating would
delete a preset's old-layout files without recreating them in the new layout
and leave the preset registry claiming artifacts that no longer exist.
Detect the intended layout via `is_skills_mode` (so a plain same-layout
upgrade is unaffected) and, when it flips while preset overrides are
installed for the agent, reject the upgrade *before any mutation* with an
actionable error pointing at the remove → upgrade → reinstall workaround.
Extension artifacts are still reconciled for the safe (no-preset) case.
Adds a regression test and documents the migration caveat in the Bob
integration reference entry.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf
* fix(bob): restrict layout reconciliation to the active integration (review #3415)
`integration_upgrade` supports upgrading a secondary (non-active) integration,
but the layout-change extension reconciliation was unsafe there.
`ExtensionManager.unregister_agent_artifacts()` treats the unscoped
per-extension `registered_skills` list as belonging to the passed agent and,
when that agent's skills directory is absent, falls back to scanning every
agent's skills directory — so reconciling a secondary Bob layout flip could
delete or untrack the *active* agent's extension skills. The subsequent
re-registration cannot repair that because extension skill rendering is
intentionally scoped to the active agent (#2948).
Gate the unregister-before-register reconciliation on `installed_key == key`
so it only runs for the active integration. Secondary agents only ever have
extension command files (skills are active-agent-only), which the existing
re-registration rewrites in place, so skipping the unregister orphans nothing
new. Adds a regression test asserting a secondary Bob layout change leaves the
active agent's extension skill intact on disk and in the registry.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf
* fix(bob): fail closed when preset registry is unreadable (review #3415)
Address review 4744636079:
- _migrate_commands: the preset guard previously failed *open* — a
registry read/parse error returned an empty "no presets" list, so a
--force layout-changing upgrade could delete preset-overridden command
files while their registry state was unknown. Read the registry file
directly and raise _PresetRegistryUnreadableError on any read/parse
failure or malformed structure, rejecting the migration before any
mutation. A genuinely absent registry still returns [] (safe).
- bob: correct the is_skills_mode docstring — upgrade *does* run setup();
disk detection is needed because legacy Bob 1.x installs never persisted
a legacy_commands option, so the stored mode is unavailable.
- tests: add fail-closed E2E (corrupted registry rejected, valid-empty
allowed) plus a unit test for _installed_presets_affecting_agent covering
absent / corrupted / malformed / valid / affecting-agent cases.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf
* fix(bob): fail closed on malformed preset entries too (review #3415)
Address review 4745191015: the preset guard read a parseable registry but
silently skipped malformed per-preset metadata and treated a malformed
registered_commands value as "no matching artifacts". A registry such as
{"presets":{"p1":[]}} therefore allowed a layout migration even though p1's
ownership is unknown, risking deletion of preset-managed files. Now raise
_PresetRegistryUnreadableError for a non-dict preset entry, a non-dict
registered_commands, or a non-list registered_skills. Extend the unit test
to cover these malformed shapes.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* feat(workflows): add standalone WorkflowResolver and overlay subsystem
Implement PR 1 of the workflow-overlays plan: a concrete, standalone
WorkflowResolver for downstream workflow extensibility without touching the
Preset subsystem.
- Add overlay manifest schema (Overlay, OverlayEdit, validate_overlay_yaml)
- Add pure-function merge engine (find_step, apply_edit, merge_steps,
validate_edits) with recursive anchor search and higher-wins semantics
- Add StepListComposer and tiered layer sources (project, installed, base)
- Add WorkflowResolver facade with inline HIGHER_WINS priority sorting
- Add CLI verbs: workflow overlay add/set-priority/enable/disable/remove/list
and workflow resolve <id>
- Wire WorkflowEngine.load_workflow through WorkflowResolver
- Extend workflow add to copy optional overlays/ subdirectory from local
workflow directories
- Add comprehensive unit, integration, and security tests
Refs: discussion #3473 (https://github.com/github/spec-kit/discussions/3473)
Assisted-by: Kimi (model: opencode-go/kimi-k2.7-code, autonomous)
* fix(workflows): reject symlinked overlay directories in layer sources
Address PR #3557 review comments r3594064534 and r3594064563:
- ProjectOverlaySource.collect now rejects symlinked per-workflow overlay
directories (.specify/workflows/overlays/<id>) before iterating
- InstalledOverlaySource.collect now rejects symlinked installed overlay
directories (.specify/workflows/<id>/overlays) before iterating
- workflow_overlay_list catches ValueError from resolver and exits with
code 1 instead of crashing on unhandled exceptions
- Added .specify/workflows/overlays to _reject_unsafe_workflow_storage
chokepoint for defense-in-depth
These guards prevent symlinked overlay directories from redirecting
auto-loaded overlay YAML to attacker-controlled content outside the
project, which could inject executable shell steps into trusted workflows.
Refs: PR #3557 review comments r3594064534, r3594064563
Assisted-by: opencode-go/qwen3.7-max (autonomous)
* fix(workflows): address Copilot review findings in merge engine
- Apply inserts before winning replace to prevent anchor-not-found errors
when replace changes step ID (r3594064604)
- Track attribution recursively for nested steps in composite inserts/replaces
so workflow resolve attributes all child steps correctly (r3594064638)
- Add regression tests for both fixes
Refs: PR #3557 review discussion
Assisted-by: GitHub Copilot (model: qwen3.7-plus, autonomous)
* refactor(workflows): simplify overlay architecture to 2-tier
Remove installed overlays tier to enforce clean separation of concerns:
- workflow add installs workflows only (no overlay copying)
- workflow overlay add installs overlays only (project-local)
Changes:
- Remove InstalledOverlaySource class and all references
- Remove overlay-copying logic from _validate_and_install_local()
- Update WorkflowResolver to 2-tier: project overlays + base workflow
- Fix --priority override timing: apply before validation, not after
- Remove tests for installed overlays (no longer applicable)
Rationale: If upstream controls both base workflow and shipped overlays,
and both get overwritten on bundle update, there's no reason to ship
overlays separately. Overlays only make sense when someone other than
the base author adds them.
Resolves all three review findings from PR #3557:
- r3594064677: workflow add no longer copies overlays from all call sites
- r3594064705: --priority override now applied before validation
- r3594064726: no stale installed overlays (tier removed entirely)
Assisted-by: Claude (model: claude-opus-4-7, autonomous)
* fix(workflows): harden overlay symlink handling
Assisted-by: GitHub Copilot (model: GPT-5.4, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(workflows): remove stale installed-overlay references from workflows.md
The 2-tier refactor (cc28185) removed the installed-overlay tier entirely,
but docs/reference/workflows.md was not updated. This commit addresses all
four Cluster 2 findings from the PR review:
- workflow add: remove sentence about copying overlays/ subdirectory
- How Overlays Work: drop installed-overlay table row and precedence prose;
rewrite to 2-tier model (project overlays only, source-order tie-break)
- overlay remove: drop trailing sentence about installed overlays
- Interaction with Bundles: rewrite to say workflow add installs only
workflow.yml; remove installed-overlay discovery language
Fixes: r3596368791, r3596368831, r3596368873, r3596368919
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(overlays): detect ancestor-conflict anchors in merge_steps
When two overlay edits target anchors that share a parent/descendant
relationship (e.g. remove an if-step + insert_after a nested child),
merge_steps processed them independently and in dict-insertion order,
making the outcome non-deterministic.
Add two private helpers to merge.py:
- _descendant_ids(step): returns all step IDs nested inside a step dict
by delegating to the existing _all_base_step_ids helper on children.
- _check_anchor_conflicts(anchors, base_steps): for each targeted anchor
finds its descendants and checks whether any other targeted anchor is
among them; returns human-readable error strings.
Wire _check_anchor_conflicts into merge_steps immediately after
edits_by_anchor is built, before any tree mutation occurs. Raises
ValueError listing the conflicting anchor pair(s) so overlay authors
know exactly what to fix.
Add TestMergeStepsAncestorConflicts (6 cases):
- remove parent + insert_after child raises ValueError
- replace parent + remove child raises ValueError
- conflict across multiple overlays raises ValueError
- sibling anchors (not ancestor/descendant) pass
- single anchor passes
- parent targeted but child not targeted passes
Closes review comment r3596368746 (PR #3557, round 2, cluster 3).
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(overlays): fix over-broad conflict detection and non-deterministic ID collision
Finding 1.1 — _check_anchor_conflicts was rejecting any ancestor/descendant
anchor pair, including insert-only edits that are perfectly safe. Only
replace/remove on an ancestor can destroy its subtree and make a descendant
anchor unresolvable. Change the signature to accept a dict[str, str]
(anchor → winning operation) and skip the check for insert_after/insert_before.
Finding 1.2 — merge_steps was calling find_step on the already-mutated tree,
so a replacement step that reused a base step ID could be accidentally targeted
by a later edit group (non-deterministic result depending on dict iteration
order). Replace the anchor-group loop with a single-pass _traverse_and_apply
that walks the original tree structure and applies edits as each step is
encountered. Anchors are never re-looked up in a mutated tree.
Design invariant enforced: overlays always apply to the original base tree and
cannot target steps introduced by other overlays. Non-remove edits on non-base
anchors now raise ValueError early.
Also removes apply_edit (no production callers, only tested in isolation) and
its test class — the new traversal inlines the same mechanics without the
find_step round-trip.
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(overlays): reject ID trailing newlines and reuse existing .yaml path
Fix two input validation bugs in the overlay layer (Group 2 of copilot
review PR #3557):
1. _validate_safe_id in schema.py used re.match() which anchors only at
the start of the string, so IDs like 'overlay\n' passed validation
and could produce newline-containing file paths. Changed to fullmatch()
so the entire string must satisfy the pattern.
2. workflow_overlay_add always wrote <id>.yml without checking whether
<id>.yaml already existed. Since the resolver loads both extensions,
this created two active layers whose edits applied twice. Now uses
the existing _find_overlay_file() to detect a pre-existing file and
reuse its path, falling back to .yml only for new overlays.
Tests added for both fixes.
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(overlays): fix display order inversion and wrap file-read errors
Finding group 3 from copilot-review-v2.md:
3.1 — Precedence display inverted (overlays/__init__.py)
collect_all_layers used a single-pass sort by (-priority, source_asc),
which placed the *losing* equal-priority source first in the display
while claiming "highest first". Fix: two-pass stable sort — source
descending then priority descending — so the actual winner (last applied
by the composer) rises to the top of the display.
3.2 — Unwrapped file-read errors (overlays/layer_sources.py)
Only yaml.YAMLError was caught around path.read_text(), so an
unreadable or non-UTF-8 overlay produced a raw traceback. Fix: widen
the except clause to (yaml.YAMLError, OSError, UnicodeDecodeError),
matching the pattern used throughout catalog.py.
Tests:
- test_workflow_resolve_equal_priority_winner_shown_first: verifies
project:zzz (the winner) appears before project:aaa in workflow resolve
output when both overlays share the same priority.
- tests/workflows/test_overlay_layer_sources.py (new): OSError and
non-UTF-8 bytes both produce OverlayLoadError, not raw tracebacks.
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: rename misleading overlay test
Assisted-by: GitHub Copilot (model: gpt-5.3-codex, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: remove EOF blank line in overlay resolver
Assisted-by: GitHub Copilot (model: gpt-5.3-codex, autonomous)
Co-authored-by: Copilot <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: handle overlay read and enumeration errors
Assisted-by: GitHub Copilot (model: gpt-5.3-codex, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(overlays): validate resolver workflow IDs
Reject unsafe and reserved workflow IDs before overlay or base sources construct paths, preventing traversal through resolver and engine fallback paths.
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot <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(overlays): drop _remove_sources_recursively from remove branch
In _traverse_and_apply, the remove branch called _remove_sources_recursively
to clean up attribution entries for the deleted step. This was inherited from
the old apply_edit loop (c70a5d6) where it was needed because the sources dict
was queried exhaustively.
In the current single-pass design, _build_attribution only traverses the result
list, so stale sources entries for removed steps are never read. The cleanup
call is therefore unnecessary — and actively harmful when another overlay has
replaced a different step with a new step that reuses the same ID: the pop
clobbers the replacement's attribution entry, causing workflow resolve to report
the surviving step as 'unknown'.
Fix: simply remove the _remove_sources_recursively call from the remove branch.
Add an attribution assertion to the existing reused-ID regression test to catch
this case.
Fixes: r3604242050 (Copilot review finding)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
* Fix CLI overlay ID validation anchoring
Use fullmatch for CLI workflow/overlay ID validation so trailing newlines are rejected consistently with manifest validation.
Add regression coverage for newline-suffixed workflow and overlay IDs in overlay set-priority.
Assisted-by: GitHub Copilot (model: gpt-5.3-codex, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): validate workflow_id in layer sources before path construction
ProjectOverlaySource.collect() and BaseWorkflowSource.collect() joined
workflow_id directly onto storage paths without validation, enabling
path traversal (e.g. '../../outside') when called outside the
WorkflowResolver.
Add _validate_workflow_id() to layer_sources.py — mirrors the same
_SAFE_ID_PATTERN / _RESERVED_WORKFLOW_IDS check used by WorkflowResolver
in overlays/__init__.py — and call it at the top of both collect()
methods before any path is constructed.
Adds parametrised tests covering unsafe IDs and verifying no filesystem
access occurs for an invalid ID.
Closes review finding r3604772700.
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): align layer source validation with _safe_workflow_id_dir
Plan §4.1 requires that Workflow-ID-Validierung, Symlink-/Containment-
Prüfungen and Fehlerübersetzung must not diverge between workflow
management and the overlay resolver.
My previous fix added ID pattern + reserved-name validation to both
collect() methods but was missing the containment step and the
BaseWorkflowSource directory/file checks that _safe_workflow_id_dir
performs.
Changes:
- Add _ensure_contained_dir(path, root) to layer_sources.py — pure
domain mirror of overlays/_commands.py::_ensure_contained_dir that
raises OverlayLoadError instead of typer.Exit
- ProjectOverlaySource.collect(): replace two inline symlink/dir checks
with _ensure_contained_dir(workflow_overlay_dir, self.overlays_dir),
adding the missing resolve().relative_to() containment step
- BaseWorkflowSource.collect(): add _ensure_contained_dir on the
workflow directory, and add workflow.yml symlink check before is_file()
The same logic now lives in three places (workflow CLI, overlay CLI,
layer sources). The DRY extraction to workflows/_validation.py is
deferred to PR 3 per plan §4.1.
Tests: add containment and symlink tests for both sources.
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <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(overlays): resolve identity from manifest field, not filename
Align overlay identity resolution with the project-wide convention:
presets use preset.id, extensions use extension.id, workflows use
workflow.id, and workflow steps use step.type_key. Overlays must
derive identity from the manifest id field, not the filename.
Rewrite _find_overlay_file() to scan all YAML files in the overlay
directory and match on the manifest id field, fixing the bug where
enable/disable/remove/set-priority failed when filename != manifest id.
Closes: PR #3557 discussion r3605010197
Assisted-by: opencode-go/qwen3.7-max (autonomous)
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(workflows): make validation behavior consistent across YAML loading paths
Address PR #3557 review finding r3607632921:
- Wrap yaml.YAMLError → ValueError in from_yaml() and from_string() so
malformed YAML matches the documented exception contract
- Add except ValueError to workflow_info to handle composition errors
cleanly instead of crashing with a raw traceback
- Remove validate_workflow() from compose() so the resolver path is
parse-only like all other YAML loading mechanisms; callers validate
explicitly via engine.validate()
- Update test to reflect new behavior: resolve() returns composed
definition, caller validates separately
Assisted-by: opencode-go/qwen3.7-max (autonomous)
* fix(overlays): list disabled overlays in management view
Keep disabled overlays visible in workflow overlay list while leaving resolution behavior unchanged.
- add an include_disabled opt-in to overlay source/resolver collection
- use include_disabled=True for workflow overlay list
- add regression tests for list visibility and default filtering
Assisted-by: GitHub Copilot (model: GPT-5.4, autonomous)
Co-authored-by: Copilot <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>
* docs: align overlay extends and resolver contract
Assisted-by: GitHub Copilot (model: GPT-5.3-Codex, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: use atomic write for overlay file updates to prevent hard-link attack
Replace in-place write_text() calls in workflow_overlay_add() and
_update_overlay_field() with the same mkstemp → write → os.replace()
pattern used by the workflow installer (_stage_workflow_file /
_commit_workflow_file / _discard_staged_workflow_file).
The prior code rejected symlinks and validated path containment, but a
hard-linked destination file passes both checks while sharing an inode
with an external file. write_text() would then truncate and overwrite
that external inode. The atomic staging approach never opens the
existing destination for writing, eliminating the hard-link vector.
Fixes findings r3608669512 and r3608669517 on PR #3557.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, supervised)
* fix(composer): preserve invalid base definition instead of coercing steps to []
When 'steps' is not a list, returning early with the unmodified
WorkflowDefinition lets validate_workflow surface the proper error
("'steps' must be a list.") to the caller. The previous silent
coercion to [] masked the validation error entirely.
Fixes: https://github.com/github/spec-kit/pull/3557#discussion_r3608669506
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, supervised)
* fix: align workflow overlay priority semantics
Assisted-by: GitHub Copilot (model: GPT-5.6 Terra, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: validate overlay priority presentation
Assisted-by: GitHub Copilot (model: GPT-5.6 Terra, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: catch OverflowError in normalize_priority for float infinity values
YAML values like `priority: .inf` parse to float('inf'), causing
int() to raise OverflowError. This broke validate_overlay_yaml()'s
'validation never raises' contract. Adding OverflowError to the
except clause makes it fall back to the default priority (10),
consistent with other invalid value handling.
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Markus <markus@example.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(extensions,presets): surface clean error on malformed download URL
`ExtensionCatalog.download_extension` and `PresetCatalog.download_pack` read
`download_url` from catalog payload data and pass it to `urlparse(...).hostname`
during the HTTPS validation. A malformed authority (e.g. an unterminated IPv6
bracket like `https://[::1`) makes urlparse/hostname raise a raw `ValueError`,
which escapes past the command handlers — they only catch `ExtensionError` /
`PresetError` — and surfaces as an uncaught traceback.
Guard the parse in a try/except and re-raise as the domain error so the CLI
reports a clean "download URL is malformed" message. Mirrors the same fix in
catalogs (#3435) and workflows/catalog.py (#3484).
Adds regression coverage for both catalogs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(presets): escape markup in preset_add error handlers
Copilot review on #3577 flagged that the malformed-URL fix stopped short:
`download_pack` now raises a clean `PresetError`, but the `preset_add`
handler rendered `{e}` unescaped. A catalog `download_url` like
`https://[not-an-ip]/x` is embedded verbatim in the message, so Rich
interprets `[not-an-ip]` as a markup tag and can raise a style/markup
exception while rendering the error — the CLI still crashes instead of
exiting cleanly.
Escape `str(e)` in the preset command handlers, matching the extension
handler at `extensions/_commands.py:657`, and hoist the `rich.markup`
import to module scope (dropping the two inline imports). Adds CLI-level
regression tests: a bracketed-host `download_url` exits cleanly, and the
compatibility/validation/error handlers escape markup-bearing messages.
Both tests fail on the pre-fix handler (test-the-test verified).
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.13.1
* chore: begin 0.13.2.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
IntegrationCatalog.add_catalog and remove_catalog re-validate the
existing catalog entries' priorities inline, separately from the base
loader. Both did `int(raw_priority)` under `except (TypeError,
ValueError)`, so a `priority: .inf` (float('inf')) raised OverflowError:
add_catalog leaked a raw traceback instead of IntegrationValidationError,
and remove_catalog crashed while building the display order.
Add OverflowError to both handlers, matching the base loader (#3525) and
the workflow/step loaders (#3526). add_catalog now raises
IntegrationValidationError; remove_catalog falls back to positional order
like the other non-integer priorities.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(workflows): reject bool/.inf catalog priority in workflow & step catalog loaders
The WorkflowRegistry and StepRegistry catalog-config loaders coerced priority
with int() inside except (TypeError, ValueError), missing two guards the base
CatalogStackBase loader already has:
- bool is an int subclass, so 'priority: true' was silently coerced to 1;
- int(float('inf')) raises OverflowError (not caught), so 'priority: .inf'
crashed with an uncaught traceback.
Add the explicit bool check and OverflowError to both loaders, and add
OverflowError to the two _coerce_priority helpers used by 'catalog add' (they
return 0 on an uncoercible existing priority instead of crashing).
Parametrized tests on both TestWorkflowCatalog and TestStepCatalog reject
priority true/false/.inf (fail before: bool coerced to 1 / inf OverflowError).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(workflows): cover add_catalog() OverflowError fallback for existing priority: .inf
The workflow/step catalog priority guards added OverflowError to _coerce_priority
(the 'catalog add' fallback), but the tests only exercised get_active_catalogs().
Add tests that prewrite an existing 'priority: .inf' entry and call add_catalog()
for both WorkflowCatalog and StepCatalog, asserting the command succeeds and the
new entry gets a valid priority (inf coerced to 0, +1). Fails before: int(inf)
OverflowError crashed add_catalog.
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(catalogs): priority: .inf yields a clean validation error, not OverflowError
_load_catalog_config coerces a catalog entry's priority with int() inside
except (TypeError, ValueError). int(float('inf')) raises OverflowError, which is
not in that tuple, so a YAML 'priority: .inf' escaped as an uncaught traceback
instead of the intended 'expected integer' validation error (the bool-is-int
case is already guarded just above). Add OverflowError to the except tuple.
Test mirrors the existing rejects_boolean_priority test with priority: .inf
(fails before: OverflowError; passes after: ValidationError naming the config).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(presets): priority: .inf in a preset catalog config yields a clean error
The PresetCatalog._load_catalog_config priority parser has its own loader
(separate from CatalogStackBase) that caught only TypeError/ValueError, so a
YAML 'priority: .inf' escaped as an uncaught OverflowError from int(float('inf')).
Add OverflowError to the except tuple (the bool-is-int case is already guarded
just above), matching catalogs.py.
Test mirrors rejects_boolean_priority with priority: .inf (fails before:
OverflowError; passes after: PresetValidationError).
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(integrations): document the 'integration list --catalog' flag
'specify integration list' accepts a --catalog flag (integrations/_query_commands.py:
typer.Option(False, "--catalog", ...)) that browses the full built-in +
community catalog, but the Integrations reference documented no options for the
list command. Add an option table for it, matching the style used by the sibling
'integration search' and 'integration catalog add' sections.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(integrations): clarify that default 'integration list' shows only built-ins
The --catalog row implied the default list already includes the full installed
set; in fact 'integration list' iterates INTEGRATION_REGISTRY (built-ins) and
marks installed status, so a community integration that is not built in only
appears with --catalog. Reword the option and the intro sentence to say the
default shows the built-in integrations and --catalog adds community ones.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`FanInStep.execute` already guards a non-list `wait_for` (#3482), and the
engine's load-time validation rejects non-string entries. But the engine does
not auto-validate step config, so on an unvalidated run `execute` iterated the
list's *elements* raw:
- An unhashable entry (a list/dict from a YAML indentation slip like
`wait_for: [[a, b]]`) crashed the whole run at `context.steps.get(entry, ...)`
with a raw `TypeError: cannot use 'list' as a dict key`.
- A hashable-but-non-string entry (`wait_for: [123]`) silently joined an empty
`{}` and still reported COMPLETED — the exact "silent empty result +
COMPLETED" wiring bug the whole-list guard and the engine's fan-in
validation both exist to prevent.
Extend the execute() guard to reject any non-string entry with the engine's
"entries must be step-id strings" phrasing, mirroring the sibling non-list
guard right above it. Adds regression coverage for unhashable and
hashable-non-string entries.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): fail fan-out loudly on a truthy non-mapping step template
A fan-out step whose `step:` is a truthy scalar or list (an authoring mistake) passed execute and reached the engine, which calls template.get("id", ...) in _run_fan_out — raising AttributeError and taking down the whole run. validate already rejects a non-mapping step, but the engine does not auto-validate, so an unvalidated run crashed.
Guard execute to FAIL the step (with a clear error and normalized empty output) instead, mirroring the existing non-list items guard and the switch non-dict cases guard. Add the matching test_execute_non_dict_step_fails_loudly covering the execute-path guard (validate was already covered).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): reject explicit fan-out `step: null` in validate()
The runtime guard in execute() rejects a truthy non-mapping step, but
`config.get("step", {})` only substitutes the `{}` default for an *absent*
key — an explicit `step: null` reaches the guard as None and FAILS the step.
validate() previously exempted None (`step is not None and ...`), so such a
workflow passed validation and then failed during execution.
Align validate() with the runtime guard: a present-but-non-mapping `step`
(including `None`) is an authoring mistake and is now rejected up front.
Extend the validate and execute regression cases to cover None.
Addresses Copilot review feedback on #3537.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`PromptStep.execute` str()-coerces `config['prompt']` and dispatches the
result to the integration CLI as the model's instructions. But its `validate`
only checked that `prompt` was *present*, not that it was a string — the exact
parity gap the sibling `ShellStep` closes for `run`.
So a YAML authoring slip like `prompt: [review, this]` or `prompt:` (null)
passed validation, then `execute` sent the Python repr (`"['review', 'this']"`,
`"None"`) to the LLM verbatim — silently wrong instructions with no error and a
COMPLETED status. The engine does not auto-validate step config
(`load_workflow` explicitly defers validation), so validation is the only place
this surfaces before dispatch.
Extend `validate` to reject any non-string `prompt` with the shell-step's
phrasing ("'prompt' must be a string, got <type>"), mirroring the shell `run`
and command `input`/`options` type checks. A `{{ ... }}` expression is still a
str, so it stays valid. Adds regression coverage for non-string prompts
(null/list/int/dict) and confirms an expression prompt still validates.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): route 'workflow status --json' errors to stderr
The workflow_status run_id error paths (FileNotFoundError -> 'Run not found',
ValueError -> invalid run) used the stdout console and fired before the
json_output branch, so 'specify workflow status <bad-id> --json' wrote a
Rich-rendered error to stdout and corrupted the JSON stream a consumer would
json.loads(). Route both through _error_console(json_output) so they go to
stderr under --json, matching the sibling 'workflow run'/'workflow resume'
commands (which use the identical RunState.load try/except) and the documented
stdout-purity contract.
Test asserts the not-found error appears on stderr and stdout stays empty under
--json (fails before: the error was on stdout).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(workflows): cover the ValueError handler in workflow status --json purity
The stderr-routing fix reroutes both the FileNotFoundError and ValueError run_id
handlers, but the test only exercised FileNotFoundError — a regression of the
ValueError path back to stdout would have gone uncaught. Add a ValueError case
(RunState.load raising) asserting the same stderr-only / empty-stdout behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Forge installs its slash-commands with hyphenated names (speckit-foo-bar, via
format_forge_command_name and the injected frontmatter name), but
ForgeIntegration inherited MarkdownIntegration.build_command_invocation, which
builds the dotted /speckit.<cmd>. So 'workflow'/command dispatch invoked
/speckit.plan while the registered command is /speckit-plan — a name Forge never
registered.
Override build_command_invocation to reuse format_forge_command_name, producing
/speckit-<name> (with '.'-to-'-' for extension commands), mirroring the skills
agents' hyphenated invocation.
Tests assert Forge core + extension invocations are hyphenated, incl. args
(fail before: dotted /speckit.plan / /speckit.git.commit).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: bump version to 0.13.0
* chore: begin 0.13.1.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
_acquire_via_az_cli runs 'az account get-access-token' with text=True, so
subprocess.run decodes stdout with the locale encoding and raises
UnicodeDecodeError (a ValueError sibling, NOT a JSONDecodeError) when the output
can't be decoded. That escaped the except (OSError, TimeoutExpired,
JSONDecodeError, KeyError) tuple and crashed a helper whose contract is to
return str | None. Add UnicodeDecodeError to the tuple.
Test patches subprocess.run to raise UnicodeDecodeError and asserts resolve_token
returns None (fails before: the error propagated).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(extensions): add assess idea assessment pipeline extension
Add a role-neutral, opt-in "Idea Assessment Pipeline" extension (id:
assess) covering the discovery work that happens BEFORE spec-driven
development. It provides a five-stage funnel: intake, research, define,
shape, decide, each writing one artifact under
.specify/assessments/<slug>/. A go verdict hands off to
/speckit.specify; killing an idea is a first-class success outcome.
Registration:
- extensions/catalog.json: bundled core opt-in entry (before bug)
- pyproject.toml: force-include maps into core_pack so it ships in the
installed wheel (verified via wheel build)
Also normalizes a Rich-wrapped substring assertion in test_workflows.py
so the suite passes at CI's 80-column non-TTY width.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
* fix(extensions): address PR review on assess extension
Resolve review feedback on github/spec-kit#3568:
- catalog.json: bump top-level updated_at to this revision (2026-07-17)
- extension.yml + catalog.json: shorten the assess description to under
the documented 200-char manifest limit (kept aligned across both)
- extension.yml: make the before_specify hook prompt condition-neutral
(it fires on every /speckit.specify, so it must not claim "no
assessment found")
- intake.md: fix slug normalization to explicitly allow lowercase
letters a-z (the old rule permitted only digits and '-', contradicting
the offline-mode example)
- intake.md + research.md: require a sanitized source URL (strip
userinfo and credential/signature query params) instead of persisting
a verbatim URL that could leak secrets into project artifacts
- decide.md: remove the "trivially small" exception so a go always
requires a shaped concept, making verdict behavior deterministic and
consistent with the guardrails and README
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
* refactor(extensions): remove before_specify hook from assess
Assess is a separate business process from spec-driven development, so
it should not inject itself into the /speckit.specify lifecycle. The
hook fired on every /speckit.specify invocation (it had no condition),
nagging even when an assessment already existed and the user was
deliberately proceeding.
Unlike git's before_specify (a mechanical prerequisite: create a feature
branch) or agent-context's after_* hooks (reacting to spec output),
assess is an upstream, optional, human-judgment process. The coupling
that belongs here already runs forward and by choice: a `go` verdict
from /speckit.assess.decide hands off to /speckit.specify. The backward
hook was the redundant, intrusive direction.
- extension.yml: drop the hooks block (commands-only manifest)
- README.md: replace the Hooks section with a Handoff section
- test: replace the hook assertion with test_declares_no_hooks to lock
in the standalone-pipeline design
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
* fix(extensions): harden assess slug handling and clarify verdict logic
Address the second review round on github/spec-kit#3568:
- Slug path traversal: intake and all four downstream commands
(research, define, shape, decide) now normalize an explicit or
user-supplied slug to the [a-z0-9-] alphabet (dropping '.', '/', '\\')
and reject an empty normalized result before constructing ASSESS_DIR.
This guarantees a slug like `../..` cannot escape .specify/assessments/.
- Metadata accuracy: the extension.yml and catalog.json descriptions no
longer imply a "build/kill" call is handed to /speckit.specify — only a
`go` hands off; a `kill` closes the assessment.
- Verdict determinism (decide): a `go` now explicitly requires evidence
strength `adequate`+ (never weak/unknown), resolving the conflict with
the thin-evidence guardrail.
- Risk polarity (decide): renamed the "Risk" criterion to "Risk posture"
with positive polarity (strong = risks understood and mitigated) so it
composes with the other scores that feed the verdict.
- README: aligned the go-threshold guardrail with the evidence rule and
documented the slug-normalization safety property.
The PR description was also updated to drop the stale before_specify
hook claim (the hook was removed in the previous commit).
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
* fix(extensions): add symlink/realpath containment and pin research host allowlist
Address the third review round on github/spec-kit#3568:
- Path safety (intake, research, define, shape, decide): slug
normalization blocks lexical `..` but not symlinked path components.
Each command now, before any mkdir/read/write, resolves the real path
of .specify/assessments/<slug>/ and every artifact, refuses to follow a
symlinked .specify / assessments / slug dir / artifact, and verifies the
resolved path stays inside the project root. This blocks a cloned or
crafted project from redirecting reads/writes outside the repository.
Each stage enforces this independently since research/define/decide can
run without intake.
- research URL policy: replaced the open-ended "and comparable well-known
hosts" no-prompt branch with intake's exact enumerated allowlist, so an
agent cannot classify an attacker-controlled host as "comparable" and
fetch it without confirmation.
- README: guardrail now documents symlink/realpath containment.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
* fix(extensions): redact secrets in captured idea and stop on explicit-slug collision
Address the fourth review round on github/spec-kit#3568 (intake):
- Secret leak in the captured idea: quoting the original "verbatim"
contradicted the URL sanitization rule when the idea itself contained a
credential-bearing URL. Capture now redacts secrets (sanitize URLs;
strip tokens, passwords, keys, cookies) inside the quoted text as well
as the Source field, and the section heading is "Idea (as captured)"
rather than "verbatim".
- Explicit-slug collision: in automated mode an existing intake.md caused
a silent switch to a new slug, contradicting the no-suffix guarantee for
user-provided slugs. Now: user-provided slug collision -> stop and
report; only a self-generated slug (already disambiguated at resolution)
is re-slugged.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
* fix(extensions): reject IPv6 private ranges and DNS-rebinding in URL policy
Address the remaining open comment from review 4722852090 on
github/spec-kit#3568 (the other six comments in that round were already
resolved by the slug-validation and host-allowlist fixes in 9cd07fb and
c032a2e).
The URL Trust Policy refused only textual IPv4 loopback/RFC1918/metadata
hosts, so an approved hostname resolving to an internal IPv6 or
IPv4-mapped address could still reach internal services. The refuse-
outright list now covers IPv6 link-local (fe80::/10), unique-local
(fc00::/7), IPv4-mapped forms, and the IPv6 metadata address, and adds a
resolution-time check: even an allowlisted or user-confirmed host is
refused when it resolves to any non-public address, defeating DNS
rebinding. Mirrored the summary in research's inherited-policy note.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
* fix(extensions): pin connection vs DNS rebinding and gate slug-only direct entry
Address the fifth review round on github/spec-kit#3568:
- DNS rebinding (intake + research): a standalone DNS lookup does not
defeat rebinding because the fetch client can re-resolve or pick a
private address from a mixed answer. The policy now requires the fetch
to pin the connection to a validated public address (or verify the
connected peer) and re-apply the refusal ranges to the address actually
connected to; if the fetch mechanism cannot pin or expose the peer, the
fetch is refused rather than trusted by hostname.
- Slug-only direct entry (research + define): when intake/research
artifacts are absent and $ARGUMENTS carries only a slug, the commands no
longer infer an idea/problem from the slug. They now require substantive
idea/problem text and otherwise prompt (interactive) or stop (automated).
- Cleaned up a leftover duplicate ASSESS_DIR assignment line in research.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
* fix(extensions): allow read-only source inspection in intake guardrail
Address the sixth review round on github/spec-kit#3568.
The intake guardrail said the command "only reads and writes inside
.specify/assessments/<slug>/", which contradicts its documented inputs:
intake must read a codebase pointer (repository inspection) and fetch an
allowed URL to capture the idea. The guardrail now limits only *writes*
to the assessment directory and explicitly permits read-only inspection
of the supplied sources (repo + allowlisted URL fetch under the URL Trust
Policy). The other four commands already phrased this correctly ("read
only, and write inside ...") and are unchanged.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
* Harden assess commands: ancestor path safety, untrusted-artifact reads, research dir creation
Addresses review 4723905370 on PR #3568 across three themes:
- Ancestor path safety: verify `.specify` and `.specify/assessments` are
real directories (not symlinks) resolving inside the project root before
any filesystem-based slug resolution, in all five commands.
- Untrusted artifact reads: treat the contents of persisted assessment
artifacts (intake/research/problem/concept) as untrusted data, not
instructions — ignore embedded directives, mirroring the URL Trust Policy.
- research now ensures the validated ASSESS_DIR exists before writing, since
it may be the first assessment command run.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
* Allow absent assessment dir in ancestor path-safety check
Addresses review 4723955260 on PR #3568. The ancestor path-safety clause
required `.specify/assessments` to already be a real directory, which blocked
the first-run commands (intake, research, define) from ever reaching the step
that creates it. Reword the clause in all five commands so a not-yet-created
directory is permitted, while still refusing when `.specify` or
`.specify/assessments` exists as a symlink or escapes the project root.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
* Fix README diagram: needs-clarification revisits the named earlier stage
Addresses review 4724027270 on PR #3568. The overview flowchart routed every
needs-clarification verdict back to research, but decide.md's Revisit stage can
send an idea back to intake, research, define, or shape. Reroute the arrow as a
generic loop back to the earlier stages so the diagram no longer misstates the
pipeline.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
* Make decide handoff integration-neutral (no hard-coded dot-style)
Addresses review 4724080304 on PR #3568:
- decide.md frontmatter description hard-coded `/speckit.specify`. Frontmatter
is parsed before command-reference resolution, so it now uses agent-neutral
wording ("hand survivors off into Spec-Driven Development") instead of a
dot-style literal that would be wrong for non-dot integrations.
- The `## If go — Handoff to …` heading inside the decision.md output template
hard-coded `/speckit.specify`, which would be written verbatim into
decision.md. It now uses the `__SPECKIT_COMMAND_SPECIFY__` placeholder, like
the rest of the command, so the active integration's invocation style is
rendered.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1f25cf9d-b7eb-4b2b-b811-3e91d8db8f6a
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
`_download_manifest` and its `_require_https` helper parsed the catalog
entry's `download_url` with an unguarded `urlparse(url)`. A malformed
authority — e.g. an unclosed IPv6 bracket like `https://[::1` — makes
`urlparse` (or `.hostname` on older Pythons) raise a raw `ValueError`. The
three `bundle` CLI commands (`info`, `install`, `update`) only catch
`BundlerError`, so that `ValueError` escaped as an uncaught traceback.
Wrap both parse sites in the same `try/except ValueError -> BundlerError`
guard already used by the sibling `_validate_remote_url` (and established by
the merged catalog-URL fix#3576), so a bad `download_url` reports a clean,
actionable error in every mode.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`PresetCatalog._validate_catalog_url` called `urlparse(url).hostname` without
guarding it. For a malformed authority such as an unterminated IPv6 bracket
(`https://[::1`), `urlparse(...).hostname` raises `ValueError: Invalid IPv6 URL`,
which escapes the method. Its docstring promises `PresetValidationError`, and its
callers (`preset catalog add`, `preset catalog list` reading the
`SPECKIT_PRESET_CATALOG_URL` env var / `.specify/preset-catalogs.yml`) only catch
`PresetValidationError` -- so a malformed URL crashes the CLI with a traceback
instead of a clean error message.
The shared `CatalogStackBase` (#3435), `workflows` (#3484), `bundler` (#3433) and
`IntegrationCatalog` copies already wrap this in `try/except ValueError`; the
preset validator was the remaining un-updated twin. Mirror the shared
implementation: wrap `urlparse` + `.hostname`, re-raise as
`PresetValidationError("Catalog URL is malformed: ...")`, and read the local
`hostname` in the host check.
Add a regression test mirroring `IntegrationCatalog`'s
`test_malformed_url_rejected_cleanly`; it is red before the fix (raw `ValueError`)
and green after.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(deps): bump github/codeql-action/init from 4.36.2 to 4.37.1
Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.36.2 to 4.37.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](8aad20d150...7188fc3636)
---
updated-dependencies:
- dependency-name: github/codeql-action/init
dependency-version: 4.37.1
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot] <support@github.com>
* chore(deps): bump github/codeql-action/analyze to 4.37.1
Bump the analyze step to match the init step (both now 7188fc3 / v4.37.1).
Dependabot bumped only init, leaving analyze on 4.36.2, which caused CodeQL
to fail with "Loaded a configuration file for version '4.37.1', but running
version '4.36.2'". Both steps must reference the same release.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: mnriem <mnriem@users.noreply.github.com>
Match the README hero tagline to the docs landing hero and rewrite the
subtitle to reflect the four-pillar positioning (ready-to-use spec-driven
process or bring your own, extensible, community-driven, org-ready) rather
than framing everything around SDD.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Copilot-Session: da32794c-5044-406c-9338-12b3ffab49f4
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* chore: bump version to 0.12.18
* chore: begin 0.12.19.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Reframe the docs landing hero and "Make it your own" pillar to inject
the "harness" and "SDLC" framing while keeping SDD front and center:
- Hero: describe Spec Kit as an extensible, intent-driven harness that
pushes any coding agent beyond code, across the SDLC or any business
process; tagline now contrasts step-by-step vs automated-workflow runs.
- "Make it your own": explain the process lives in swappable building
blocks (not locked to SDD or even software) and add a real non-software
preset (Fiction Book Writing) to back the broadened scope.
- Community blurb: drop "development" so "entirely new processes" matches
the wider positioning.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Copilot-Session: 1cf71797-ac0d-4a5e-8266-784906933b54
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* docs: reframe SDD positioning, modernize install, and de-duplicate walkthroughs
Reframe the landing page so Spec Kit reads as a toolkit for Spec-Driven
Development *or your own process* with any AI coding agent, and correct
stale claims: context files and git are now opt-in extensions, and the
install path uses PyPI (specify-cli). Generalize the landing cards to
cover bundles and catalog hosting across all primitives.
Restructure the Quick Start into a lean, guided Taskify walkthrough with
one command per step (install as a prerequisite, Steps 1-9 aligned with
the Full path), and extract the deep per-command detail into two new
reference pages: reference/agentic-sdd.md (the /speckit.* SDD process)
and reference/agentic-bugfix.md (the bug extension). Retitle the
reference overview to "Reference" and group these agentic processes in
their own section, distinct from CLI-managed primitives.
Remove the duplicated "Detailed Process" walkthrough from README.md
(and its TOC entry), repointing readers to the docs-site Quick Start
while keeping the concise "Get Started" section as the front door.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9d9232f8-ece4-4aa6-a9bd-ff8d74ca1c89
* docs: address review feedback on accuracy and scope
- quickstart: correct the git/feature note — resolution reads
.specify/feature.json / SPECIFY_FEATURE, not the checked-out branch,
so switching branches alone does not switch the active feature.
- quickstart + agentic-sdd: add an invocation-style note ($speckit-* for
Codex/ZCode, /skill:speckit-* for Kimi) so the agent-neutral commands
are executable everywhere.
- agentic-sdd: fix the tasks phase structure to match the generator
(Setup, Foundational, one phase per user story, final Polish; tests
optional within user-story phases).
- index: soften the catalog claim (catalogs curate discovery, not an
install allow-list) and relabel the "CLI reference" link to "Reference"
to match the retitled, broader page.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9d9232f8-ece4-4aa6-a9bd-ff8d74ca1c89
* docs: correct feature-resolution override and add bug-command invocation note
- quickstart: the previous fix named the wrong override. The active
feature *directory* resolves from SPECIFY_FEATURE_DIRECTORY then
.specify/feature.json; SPECIFY_FEATURE only supplies the identifier
after a directory is resolved. Rewrite the note to point users at
.specify/feature.json / SPECIFY_FEATURE_DIRECTORY, and clarify the git
extension's branches don't by themselves change the active feature.
- agentic-bugfix: add the same invocation-style caveat as the SDD
reference ($speckit-bug-* for Codex/ZCode, /skill:speckit-bug-* for
Kimi).
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9d9232f8-ece4-4aa6-a9bd-ff8d74ca1c89
* docs: tighten bug-command contracts and drop numbered-phase examples
- agentic-bugfix: don't overstate overwrite protection — an interactive
run can overwrite an existing assessment after confirmation; only
automated mode refuses and picks a new slug. Correct the verify verdict
to the schema's verified/partial/failed (not-run is a per-check status);
an unexercised reproduction downgrades the result to partial.
- agentic-sdd: the implement examples labeled scoping "Phase 1/2", but
the tasks contract reserves Phase 1 for Setup and Phase 2 for
Foundational (user stories start at Phase 3). Scope by phase name and
user-story content instead to avoid mis-scoping execution.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9d9232f8-ece4-4aa6-a9bd-ff8d74ca1c89
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* docs: refresh landing page ecosystem stats
Update stale numbers in docs/index.md to match current catalogs on
upstream/main and live GitHub data: extensions 105->138, presets
22->25, integrations 30+->35, contributors 200+->240+, friends 4->6,
GitHub stars 106K+->121K+, extension authors 60+->70+, and the
last-updated date.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e5f34221-4e6c-42e8-9fa3-5cfbc26104d1
* docs: align community extension stats on overview page
Update docs/community/overview.md from "Over 90 ... 50+ authors" to
"Over 130 ... 70+ authors" so it matches the refreshed landing-page
numbers in docs/index.md (137 community extensions, 77 unique authors).
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 134166d9-e599-44fa-a88d-daf84ab6aca6
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* chore: bump version to 0.12.17
* chore: begin 0.12.18.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(workflows): fail if/switch steps on non-list branch instead of crashing
`IfThenStep.validate()` and `SwitchStep.validate()` already reject a
non-list branch (`then`/`else`, and `case`/`default`), 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, the selected branch is fed
straight into `next_steps`, which `_execute_steps` iterates as step
mappings. A non-list branch — a single mapping or scalar authoring
mistake — was iterated element-wise (a dict yields its string keys, a
str its characters) and raised `AttributeError` on `.get()`, taking down
the whole run; the engine invokes `step_impl.execute()` with no
surrounding try/except.
Guard both `execute` paths to return a FAILED StepResult naming the type
error instead, mirroring the switch non-mapping `cases` and fan-out
non-list `items` handling. The switch guard is factored into a shared
`_non_list_branch_failure` helper covering both `case` and `default`
branches. A missing `else`/`default` still defaults to an empty list
(COMPLETED), unchanged; the guard fires only on an explicit non-list
value. The condition/expression is still evaluated first, so its result
is surfaced in the step output for downstream context.
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>
* test(workflows): cover switch non-list branch execute paths
Copilot flagged the new switch branch guards as untested: coverage
stopped at a non-mapping `cases` container. Add SwitchStep.execute
tests for a matched case with a non-list body and a non-list default
(dict/str/int), asserting FAILED, the branch-specific error, empty
next_steps, and preserved expression_value. Also add explicit
`default: null` / `else: null` normalization tests so the
validator-approved empty-branch contract cannot regress.
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(integrations): add Grok Build skills-based integration
Add first-class support for xAI Grok Build via SkillsIntegration, installing
speckit skills under .grok/skills and wiring init/invocation/catalog surfaces.
Assisted-by: Grok Build (model: grok-4, supervised)
* test+docs: address Copilot review on Grok multi-install and next steps
Assert init next-steps guidance for Grok (.grok/skills, /speckit-*) and
clarify that multi-install safety is path/manifest isolation, not
agent-context defaults such as shared AGENTS.md.
* fix(integrations): Grok headless --always-approve and isolation paths
Document Grok multi-install isolation as .grok/skills and .grok/rules.
Override build_exec_args to pass --always-approve so non-interactive
dispatch is not blocked at tool permission gates.
* docs(integrations): list only managed .grok/skills for Grok isolation
Multi-install isolation documents Spec Kit-managed paths; Grok only
writes .grok/skills, so drop the read-only .grok/rules entry.
* fix(integrations): always-slash Grok hooks and refresh catalog date
Move grok to ALWAYS_SLASH_AGENTS so hooks never emit /speckit.plan when
ai_skills is missing/false. Update slash-format tests, persist ai_skills
on init, and bump catalog updated_at for the Grok entry.
---------
Co-authored-by: Nate Chadwick <1232206+natechadwick@users.noreply.github.com>
Co-authored-by: test <test@example.com>
The bash and Python twins validate --number against ^[0-9]+$ and reject a
negative value with 'Error: --number must be a non-negative integer'. The
PowerShell twin declares the parameter as [long]$Number, so PowerShell binds
'-5' as -5 instead of rejecting it. That value then formats via '{0:000}' to
'-005' and yields a branch name starting with a dash, which git refuses (refs
cannot begin with '-') — a confusing late failure instead of the twins' clear
early error.
Guard for $Number -lt 0 up front (before the description check, matching the
bash twin's parse-time validation order) and emit the identical error. An
explicit -Number 0 is still honored, preserving the #3412 fix.
Add matching negative-number parity tests to the bash and PowerShell
create-feature suites, mirroring the existing test_explicit_number_zero_is_honored
pair. Same PowerShell-parity bug class as #3412.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix preset-constitution-not-installed: use PresetResolver in constitution setup
Apply the remediation from the bug assessment on issue #3272.
Changes:
1. Modify ensure_constitution_from_template (init.py) to resolve the
constitution-template through the preset priority stack via
PresetResolver, instead of hardcoding the core template path. This
ensures a preset's replacement constitution-template is used when
seeding .specify/memory/constitution.md.
2. Reorder init flow: move ensure_constitution_from_template to after
the preset installation block so that 'specify init --preset' seeds
the memory file from the already-resolved template stack, not from
the generic template that existed before the preset arrived.
3. Add _maybe_reseed_constitution to PresetManager (presets/__init__.py):
a post-install hook that re-seeds .specify/memory/constitution.md
from the preset's constitution-template during 'specify preset add'
on an existing project, but only when the memory file still contains
generic placeholder tokens ([PROJECT_NAME] or [PRINCIPLE_1_NAME]).
Legitimately authored constitutions (no placeholder tokens) are never
overwritten.
4. Add regression tests covering both code paths (TestConstitutionReseedOnPresetInstall
and TestEnsureConstitutionFromTemplate in tests/test_presets.py).
Refs #3272
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Harden preset constitution resolution
Use manifest-aware composed content, atomic safe writes, and conservative generic-template matching for constitution seeding and re-seeding.
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 49891a32-bec4-462c-a7f2-6d6ec4eefcdb
* Limit preset CLI change to regression test
Remove accidental whole-file Ruff formatting introduced during conflict resolution so the PR contains only the intended end-to-end test.
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 49891a32-bec4-462c-a7f2-6d6ec4eefcdb
* Make preset init test depend on init ordering
Disable preset-install lifecycle seeding in the regression test so it fails unless init materializes the constitution after registering the preset.
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 49891a32-bec4-462c-a7f2-6d6ec4eefcdb
---------
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: Ben Buttigieg <70525+BenBtg@users.noreply.github.com>
`specify integration use copilot` against a Copilot install configured with
`--integration-options "--skills"` dropped `"ai_skills": true` from
init-options.json and regenerated extension commands in the legacy
`.agent.md`/`.prompt.md` layout, contradicting `integration.json`'s stored
`parsed_options.skills: true`.
`_update_init_options_for_integration` only inspected `SkillsIntegration` /
the instance `_skills_mode` flag. On the `use` path no `setup()` runs, so the
freshly-resolved Copilot instance has `_skills_mode == False` and the stored
skills intent in `parsed_options` was ignored. Thread the resolved
`parsed_options` through and treat `parsed_options["skills"]` as skills mode.
Adds a regression test that resets the registry singleton's `_skills_mode` to
simulate a fresh process (in-process singleton reuse otherwise masks the bug).
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Copilot-Session: 06fb6ae9-f444-4dfd-ab3f-d0669c5d0604
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Add Figma Starter extension to community catalog
Add figma-starter extension submitted by @vibhus to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table
Closes#3545
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: add missing python3 >=3.8 version constraint in figma-starter catalog entry
Assisted-by: GitHub Copilot (model: claude-sonnet-4.5, autonomous)
* fix(agent-context): reduce Python subprocess overhead in PS 5.1 YAML fallback
The PowerShell update-agent-context.ps1 script falls back to Python when
PS 5.1 (which lacks ConvertFrom-Yaml and cannot parse YAML as JSON) reads
the extension config. It previously launched Python twice: once to verify
that Python 3 + PyYAML were available, and once to run a temp script file
that parsed the YAML and printed JSON.
On Windows CI each Python startup—plus potential Windows Defender scanning
of a freshly-created .tmp file—can take several seconds. With two launches
plus PS 5.1's own startup time the subprocess.run(timeout=30) threshold in
test_powershell_script_discovers_nested_plan was regularly exceeded, causing
the CI job to fail.
Replace the two-phase approach with a single Python -c one-liner that
verifies PyYAML availability, parses the YAML file, and emits JSON in one
process. This halves the number of Python launches and eliminates the temp
file entirely, keeping total execution time well under 30 s.
Assisted-by: GitHub Copilot (model: claude-sonnet-4.5, autonomous)
* revert(agent-context): restore update-agent-context.ps1 to pre-optimization state
The runtime optimization (one-liner Python fallback, stderr suppression) was
unrelated to the Figma Starter catalog addition (issue #3545) and removed the
actionable PyYAML/parse diagnostic messages. Revert the file so the PR only
contains the catalog-entry and documentation changes.
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.16
* chore: begin 0.12.17.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
The `map`, `join`, and `contains` expression filters assumed their
argument was a string. A non-string argument — an authoring mistake such
as `| map(5)`, `| join(5)`, or `| contains(5)` — reached an operation
that only strings support and raised a cryptic exception that escaped the
evaluator entirely:
* `map(5)` -> `attr.split(".")` -> AttributeError
* `join(5)` -> `separator.join(...)` -> AttributeError
* `contains(5)` on a string value -> `x in str` -> TypeError
The engine wraps neither expression evaluation nor `step_impl.execute()`
in a try/except, so each of these took down the whole run with a message
that names none of the real problem.
Validate the argument type up front and raise a `ValueError` naming the
filter and the offending type instead, mirroring the strict argument
handling already in `from_json`. `contains` guards only the string-value
branch: for a list value, membership of any element type is legitimate
(`5 in [1, 2, 5]`), so that branch is intentionally left unguarded.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(workflows): expose workflow source directory to steps (#3467)
Propagate WorkflowDefinition.source_path to steps via
{{ context.workflow_dir }} in template expressions and
SPECKIT_WORKFLOW_DIR env var for shell steps. The original
source directory is persisted in state.json so resume
restores the correct value instead of the run-directory copy path.
Closes#3467
Assisted-By: 🤖 Claude Code
* fix: apply bot review suggestions (#2)
Applied fixes from bot review comments:
- Comment #3563319058: prevent stale SPECKIT_WORKFLOW_DIR leak from parent env
- Comment #3563319094: use cross-platform Python one-liner instead of printenv
- Comment #3563319103: add monkeypatch.delenv for deterministic env var test
- Comment #3563319116: same env leak fix as #3563319058
Assisted-By: 🤖 Claude Code
* fix: use YAML single-quotes and forward-slash paths for Windows CI (#2)
sys.executable on Windows returns backslash paths (D:\a\...) which YAML
double-quoted strings interpret as escape sequences. Switch to
single-quoted YAML strings and normalize paths with replace("\\", "/").
Assisted-By: 🤖 Claude Code
* fix: resolve workflow_dir to absolute path and add installed-by-ID test (#3469)
Applied fixes from bot review comments:
- Comment #3563382853: resolve source_path before taking parent to ensure absolute paths
- Comment #3563382864: add test for installed-by-ID workflow_dir semantics
Assisted-By: 🤖 Claude Code
* docs: document context.workflow_dir and SPECKIT_WORKFLOW_DIR
Add reference documentation for the new workflow_dir runtime value in
both workflows/README.md and docs/reference/workflows.md so workflow
authors can discover the feature and its semantics.
Assisted-By: 🤖 Claude Code
* fix: clarify installed workflow_dir is an absolute path (#3469)
The documentation for context.workflow_dir described the installed-by-ID
case as ".specify/workflows/<id>/" which appears relative, contradicting
the "resolved absolute path" semantics. Clarified that it is the absolute
path to the installation directory.
Assisted-By: 🤖 Claude Code
* fix: apply bot review suggestions (#3469)
Applied fixes from bot review comments:
- Comment #3580005128: Quote sys.executable in shell step env var test
- Comment #3580005174: Quote sys.executable in no-env-var test
Assisted-By: 🤖 Claude Code
* fix: apply bot review suggestions (#3469)
Applied fixes from bot review comments:
- Comment #3587146944: Quote interpolated workflow_dir path in example
Assisted-By: 🤖 Claude Code
_run_fan_out coerces max_concurrency with int() inside except (TypeError,
ValueError). int(float('inf')) raises OverflowError, which is not in that tuple,
so a YAML 'max_concurrency: .inf' crashed the whole run with an uncaught
OverflowError instead of the documented 'cannot be coerced -> sequential'
fallback. Add OverflowError to the except tuple (nan already coerced via
ValueError).
Extends the existing invalid-value parametrization with float('inf')/nan (fails
before on inf: OverflowError; passes after: sequential, all items in order).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(presets): seed constitution from preset constitution-template (#3272)
The constitution is the only template materialized to a live file
(.specify/memory/constitution.md) rather than resolved on demand, yet
ensure_constitution_from_template hardcoded a copy from the core template
and ignored PresetResolver. Combined with init seeding the constitution
before preset installation, a preset's constitution-template (e.g.
strategy: replace with a ratified constitution) could never go live.
Changes:
- ensure_constitution_from_template now resolves constitution-template
through PresetResolver, so a preset/override/extension wins and core is
the fallback.
- init seeds the constitution after preset installation so init --preset
uses the resolved stack.
- install_from_directory re-seeds memory/constitution.md from the resolved
preset template, guarded to only act when the memory file is missing or
still contains generic placeholder tokens — authored constitutions are
never overwritten. Covers preset add and install_from_zip.
- Tests for preset seeding, placeholder re-seed, authored-constitution
preservation, override resolution, and resolver-aware init seeding.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(presets): compose constitution-template when seeding memory
Take on review feedback from Copilot and gglachant:
- constitution seeding previously copied the top layer file path verbatim
even when the winning layer used a composing strategy
(prepend/append/wrap), which could leave {CORE_TEMPLATE} unresolved.
- both seeding paths now inspect resolver layers and only copy verbatim for
replace; non-replace strategies materialize composed content via
PresetResolver.resolve_content().
- add regression tests for wrap strategy composition in both
PresetManager seeding and ensure_constitution_from_template.
- add a drift-guard test pinning _CONSTITUTION_PLACEHOLDER_TOKENS to the
placeholders in templates/constitution-template.md.
Assisted-by: GitHub Copilot (model: GPT-5.3-Codex, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* refactor(presets): unify constitution template materialization
Address latest Copilot feedback on the constitution seeding path:
- moved resolver/layer I/O behind the existing-memory fast path in init
- corrected tracker output for composed materialization
- deduplicated materialization logic shared by init and preset install seeding
into presets._materialize_constitution_template()
Behavior is unchanged for replace strategies (copy verbatim) and remains
composed for prepend/append/wrap via resolve_content().
Assisted-by: GitHub Copilot (model: GPT-5.3-Codex, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(init): restore shutil import
The constitution materialization refactor removed the module import, but init
still uses shutil.rmtree when cleaning up a failed new-project initialization.
Restore the import so the required ruff check passes.
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(presets): harden constitution materialization
Address the outstanding review batch for preset constitution seeding:
- use checked atomic writes and reject symlinked memory paths
- replace placeholder heuristics with hash/source provenance
- rematerialize unchanged generated constitutions by resolver priority
- preserve authored or edited constitutions, including placeholder mentions
- warn non-fatally when post-install materialization cannot complete
- retain exact core-template comparison for legacy projects without provenance
Add focused provenance, priority, symlink, and failure-path coverage, and
update integration inventories for the generated provenance sidecar.
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1b2c095d-b45c-4d52-8d56-bd6121d96ab6
* fix(presets): reconcile constitution after removal
When the removed preset supplied constitution-template, rematerialize the
winning remaining resolver layer only if provenance proves the live file is
still generated and unchanged. Preserve edited constitutions and report
post-removal reconciliation failures as non-fatal warnings.
Add coverage for restoring the core layer, falling back from a removed
higher-priority preset, and preserving edited generated content.
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1b2c095d-b45c-4d52-8d56-bd6121d96ab6
* fix(presets): tighten legacy constitution provenance
Trust only the immutable bundled/source constitution template when migrating
legacy projects without provenance. Do not infer core provenance from mutable
project templates or preset source labels, including IDs beginning with core.
Also detect convention-based constitution-template files before preset removal
so unchanged generated constitutions reconcile to the next resolver layer.
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1b2c095d-b45c-4d52-8d56-bd6121d96ab6
* fix(presets): preserve files with invalid provenance
Use immutable-core legacy migration only when the provenance sidecar is absent.
If a sidecar is malformed or its hash does not match the live constitution,
treat the file as edited and preserve it.
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1b2c095d-b45c-4d52-8d56-bd6121d96ab6
* fix(presets): reconcile constitution on stack changes
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1b2c095d-b45c-4d52-8d56-bd6121d96ab6
* fix(presets): guard constitution reconciliation edges
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1b2c095d-b45c-4d52-8d56-bd6121d96ab6
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* docs: add PyPI as second supported install route (#3425)
The specify-cli package is now officially published to PyPI via the
publish-pypi.yml trusted-publishing workflow. Document PyPI as a
supported install route alongside the GitHub source install:
- Revise the outdated "not affiliated" warning in installation.md to
reflect that specify-cli on PyPI is an official, maintained channel.
- Add an "Install from PyPI" section and list PyPI under alternative
package managers.
- Add a dedicated docs/install/pypi.md guide (install, pin version,
verify, upgrade, uninstall).
- Add the PyPI guide to the docs TOC.
- Mention the PyPI route in the README quick start.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* docs: refine PyPI install guidance from review (#3516)
Address review feedback for the PyPI install documentation:
- Reword the verification guidance so `specify version` is described as a
local version/runtime check rather than proof of package provenance.
- Clarify that upgrading a pinned `uv tool` install to the newest PyPI
release requires an unpinned reinstall command.
- Note that `specify self upgrade` rebuilds `uv tool` and `pipx`
installs from the GitHub source release URL rather than preserving a
PyPI-based installation.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* docs: clarify PyPI verification and upgrade guidance
Assisted-by: GitHub Copilot (model: MAI-Code-1-Flash, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* docs: point PyPI provenance check to source metadata
Address review feedback: version/list commands do not reveal install
provenance. Direct readers to the source metadata their package manager
records (pipx list --json, PEP 610 direct_url.json) to confirm whether an
install came from PyPI or a Git URL, and note pip show cannot see
uv/pipx-managed environments.
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>
`WhileStep.validate()` and `DoWhileStep.validate()` already reject a
non-list `steps` body, 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 the body
is returned as `next_steps`, and the engine feeds it straight into
`_execute_steps`, which iterates it as step mappings. A non-list `steps`
— a single mapping or scalar authoring mistake — was iterated
element-wise (a dict yields its string keys, a str its characters) and
raised `AttributeError` on `.get()`, taking down the whole run; the
engine invokes `step_impl.execute()` with no surrounding try/except.
Guard both `execute` paths to return a FAILED StepResult naming the type
error instead, mirroring the if/switch non-list-branch and fan-out
non-list `items` handling. The do-while body always dispatches on the
first call, so its guard is unconditional; the while body only
dispatches when the condition is truthy, so its guard fires only then —
a false condition leaves a non-list `steps` benign and the step
completes, unchanged. The condition/expression is still evaluated first,
so its result is surfaced in the step output for downstream context.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(extensions): port git extension scripts to Python
Ports git-common, initialize-repo, auto-commit, and
create-new-feature-branch to extensions/git/scripts/python/,
mirroring the bash/PowerShell twins. Parity tests run each bash
script and its Python twin in identical projects and compare
output, exit codes, and resulting git state.
Fixes#3282
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: match bash error message for whitespace-only descriptions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Handle unreadable git-config.yml and assert stderr parity
An unreadable config file raised OSError with a full traceback from
_parse_auto_commit_config. Treat it like a missing config: auto-commit
stays disabled. Covered by a chmod-000 test (skipped on non-POSIX and
as root).
_assert_parity now also compares stderr so warning or usage-text
regressions between the bash and Python twins fail the suite. All
existing parity tests pass with the stricter assertion.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(extensions/git): pass script path to core.get_repo_root for cwd-outside-repo callers
Without script_file, core.get_repo_root() falls back to Path.cwd() when
SPECIFY_INIT_DIR is unset and no .specify root is found upward — the
bash twin instead falls back to the script's install location
(.specify/scripts/...). Pass script_file so both twins resolve the
same repo_root; TypeError fallback keeps older cores working.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: exercise SPECIFY_INIT_DIR from outside the project
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(extensions/git): handle UnicodeDecodeError and USER/USERNAME fallback
- Catch (OSError, UnicodeDecodeError) when reading git-config.yml in
create_new_feature_branch.py, initialize_repo.py, and auto_commit.py
so invalid UTF-8 config falls back to defaults instead of crashing
with a traceback.
- Fall back to USERNAME (then "unknown") when USER is unset when
deriving the branch author token, matching the PowerShell twin's
Windows-friendly fallback chain.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(extensions/git): platform-aware persist hint and stronger SPECIFY_INIT_DIR test
- Add a shared _persist_hint() helper in create_new_feature_branch.py
and use it for both the JSON-mode stderr hint and the human-readable
stdout hint, so there is a single place emitting the SPECIFY_FEATURE
persistence guidance. On Windows (os.name == "nt") it prints
PowerShell $env:VAR = "..." syntax; elsewhere it keeps the existing
POSIX export VAR=... syntax (parity with the bash twin).
- Rework test_specify_init_dir_resolves_target_project so SPECIFY_INIT_DIR
is the only thing that can produce the observed result: the script now
runs from a separate host_proj (no existing specs, so script/cwd-based
discovery would yield 001) while SPECIFY_INIT_DIR points at a different
target_proj that already has an existing spec (007-existing, so the
override must yield 008). The old version pointed SPECIFY_INIT_DIR at
the same project the script was installed in, so it passed even if the
env var were ignored.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(extensions): tolerate missing Git executable
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(extensions): quote PowerShell persist hint
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(git): match bash persist hint escaping
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(git): ignore unterminated config record
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(git): handle Windows persist hint parity
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(init): install Python shared scripts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(git): normalize Windows persistence hints
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.15
* chore: begin 0.12.16.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* 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>
* feat: add Firebender integration (Android Studio / IntelliJ)
Firebender (https://firebender.com/) is an AI coding agent for Android
Studio and IntelliJ. It reads project-local custom slash commands from
.firebender/commands/*.mdc and project rules from .firebender/rules/*.mdc.
Add a FirebenderIntegration (MarkdownIntegration) that installs the
speckit command templates as .mdc command files and writes the managed
context section into .firebender/rules/specify-rules.mdc. command_filename
is overridden so init-time commands also use the .mdc extension Firebender
requires. Register it in the integration registry, add the catalog entry
and docs row, and add an integration test covering the .mdc command output.
Closes#1548
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: address review - bump catalog updated_at and list firebender as multi-install safe
Bump the catalog top-level updated_at to reflect the new entry, and add firebender (with its .firebender/commands + .firebender/rules/specify-rules.mdc isolation paths) to the 'currently declared multi-install safe integrations' table in the docs.
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(shared-infra): remove stale managed scripts the core no longer ships (#3076)
install_shared_infra never removed shared scripts a prior (pre-refactor) install recorded but the current core no longer ships — e.g. the legacy scripts/<variant>/update-agent-context.sh, superseded by the bundled agent-context extension. On a legacy project the orphan lingers and crashes when it sources a refreshed common.sh (HAS_GIT unbound under set -u).
Apply the stale-removal that integration_upgrade already performs to install_shared_infra: manifest-tracked scripts the current bundle no longer produces are removed, but only managed copies (hash matches the manifest); user-customized files, symlinks, and recovered entries are preserved. Guarded so a missing/empty source can't trigger mass deletion, and the safe-destination check prevents unlinking through a symlinked ancestor.
Add IntegrationManifest.remove(); drop the stale update-agent-context.sh reference in CONTRIBUTING.md.
AI assistance: implemented with Claude Code (Anthropic); reviewed and validated locally (ruff clean, full suite 4176 passed, manual CLI repro).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(shared-infra): harden stale-cleanup per review (empty source + orphan manifest)
- Set scripts_scanned only after a real source file is seen, so an empty variant source can't trigger mass deletion of tracked scripts.
- Prune a stale manifest entry even when its file is already gone from disk, keeping the manifest consistent (previously left tracked forever).
- Add a test for each edge case.
Addresses the Copilot review comments on #3098. AI assistance: Claude Code (Anthropic), reviewed/validated locally (ruff clean, full suite 4178 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(shared-infra): guard unsafe manifest keys in stale-cleanup (review)
- Skip absolute / '..' manifest keys before any filesystem access in stale-cleanup, so a corrupted/hand-edited manifest can't make it touch paths outside the project root (mirrors IntegrationManifest.check_modified / uninstall).
- Clarify the scripts_scanned comment: the safety hinge is that flag, not seen_rels (which also holds template paths).
- Add a containment test: a traversal manifest key is skipped, its target untouched.
Addresses the second round of Copilot review on #3098. AI assistance: Claude Code (Anthropic); validated locally (ruff clean, full suite 4179 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(manifest): make remove() reject absolute/.. keys like its siblings (review)
IntegrationManifest.remove() now applies the same lexical validation and normalization as record_existing() / is_recovered(): absolute paths and '..' segments are rejected (return False) instead of being used verbatim as a key. Keeps the manifest API consistent. Adds tests (valid drop + no-op, absolute rejected, traversal rejected).
Addresses the third round of Copilot review on #3098. AI assistance: Claude Code (Anthropic); validated locally (ruff clean, full suite 4182 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(shared-infra): validate stale-cleanup keys for containment, not just lexically (review)
The stale-script cleanup guarded manifest keys with a lexical check only
(is_absolute() / ".." segments). On Windows a drive-relative key such as
"C:tmp\\file" is not is_absolute(), yet joining it onto the project path
discards the root — so cleanup could stat/unlink outside the project before
_ensure_safe_shared_destination raised, and a corrupted manifest key turned
into an install-time hard failure (ValueError) instead of being skipped.
Reuse the canonical containment helper (_validate_rel_path, the same one
IntegrationManifest.is_recovered / remove use): after the fast lexical reject,
resolve the join and confirm it stays within the project root; a key that
still escapes is skipped, never unlinked, never fatal.
Adds a regression test that forces _validate_rel_path to reject a managed key
(portably simulating the Windows drive-relative escape) and asserts the
install skips it without failing and still installs the real scripts.
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.5
* chore: begin 0.11.6.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix: register enabled extensions for agent on integration install/upgrade
install and upgrade only set up the integration's own core commands; only
switch re-registered the enabled extensions' commands for the target agent.
A second integration added via install (or refreshed via upgrade) was
therefore silently missing the extension commands the existing agents
already had (e.g. the bundled agent-context extension).
Extract switch's registration into a shared _register_extensions_for_agent
helper and call it from install and upgrade too, so every installed agent
ends up with every enabled extension's commands — full parity with switch.
Closes#2886
* test: pin skills-mode secondary-agent registration; document #2948 limitation
Extension skill rendering is scoped to the active agent (init-options track a
single ai / ai_skills pair), so a skills-mode agent registered while not active
(e.g. Copilot --skills installed as a secondary integration) gets command files
rather than skills. install/upgrade match extension add here; only switch
renders skills, because it activates the target first.
Add a regression test pinning this behavior and document the limitation on the
shared helper. Per-agent skills parity is tracked separately in #2948.
* fix: don't re-render the active agent's skills when registering a non-active agent
register_enabled_extensions_for_agent runs an active-agent-scoped skills pass
(_register_extension_skills resolves the skills dir from init-options["ai"],
ignoring the passed agent). Routing install/upgrade of a secondary integration
through it re-rendered the *active* skills-mode agent's extension skills as a
side effect — resurrecting skill files the user had deliberately deleted. Gate
the skills pass on the target being the active agent; switch is unaffected
because it activates the target first.
Also harden the skills-mode install test (assert a core skill so --skills is
load-bearing, drop a vacuous registered_skills assertion) and add a regression
test. Surfaced by review of the PR; skills parity for non-active agents stays
tracked in #2948.
* refactor: share the extension-op scaffold and run (un)registration post-commit
Review cleanups, no behavior change on the success path:
- Extract the best-effort ExtensionManager scaffold (lazy import, instantiate,
except -> _print_cli_warning) into _best_effort_extension_op. Both
_register_extensions_for_agent and a new _unregister_extensions_for_agent
delegate to it, removing the duplicate block left inline in switch.
- Invoke the best-effort extension registration AFTER the install/switch/upgrade
try/except has committed, so a failure in it can never trigger the rollback
(install and switch teardown on except).
* docs: clarify extension registration parity scope
* fix(integrations): defer extension registration until use
* fix(tests): remove redundant shutil import
* fix(integrations): backfill extensions for installed switch targets
* feat: add PyPI publishing workflow and readme metadata
- Add readme = "README.md" to pyproject.toml for PyPI project description
- Add manual publish-pypi.yml workflow using trusted publishers (OIDC)
- Update release.yml install instructions to prefer PyPI
The publish workflow is manually triggered after a release, checks out the
specified tag, verifies version consistency, builds with uv, and publishes
using trusted publishing (no API tokens required).
Prerequisites before first use:
- Take ownership of the specify-cli PyPI project (#2908)
- Create a 'pypi' environment in repo settings
- Configure trusted publisher on PyPI for this repo/workflow
Closes#2908
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address PR review feedback on publish workflow
- Add actions: read permission (required for artifact upload/download)
- Move version check after uv install and use uv run python (ensures
Python >=3.11 with tomllib is available regardless of runner image)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: use absolute URLs for README images (PyPI compatibility)
PyPI does not host images from the repository, so relative paths like
./media/logo.webp render as broken images. Switch to absolute
raw.githubusercontent.com URLs so images display on both GitHub and PyPI.
Ref: https://github.com/pypi/warehouse/issues/5246
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address second review round
- Convert remaining /media/ image path to absolute URL for PyPI
- Pin release install to specific version (specify-cli==X.Y.Z)
- Align setup-uv to v8.2.0 matching rest of CI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address third review round
- Use job-level permissions: actions:write on build (for upload-artifact),
actions:read on publish (for download-artifact)
- Include both @latest and pinned version in release notes
- Add note that PyPI may lag behind the GitHub release
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: add contents:read to build job, clarify manual publish
- Build job needs contents:read for checkout (job-level perms replace
workflow-level)
- Clarify that PyPI publishing is manually triggered, not automatic
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: force tag resolution and validate before checkout
Move tag format validation before checkout and use refs/tags/ prefix
to ensure we always check out a tag, not a branch with the same name.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address review - links, install cmd, python pin
- Convert all relative .md links in README to absolute GitHub URLs
for PyPI rendering compatibility
- Fix release notes: use 'uv tool install specify-cli' (no @latest)
- Pin Python 3.13 via uv python install for deterministic builds
and use python3 directly instead of uv run
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address review - python setup, docs alignment, publish flag
- Use actions/setup-python (pinned v6, Python 3.13) instead of
uv python install for deterministic builds
- Use python instead of python3 for setup-python compatibility
- Remove unsupported --trusted-publishing always flag from uv publish
(OIDC is auto-detected with id-token: write)
- Update README install to lead with PyPI, source as fallback
- Update installation guide: replace PyPI disclaimer with official
package note, add PyPI as primary install method
- Release notes: pin to exact version, clarify PyPI timing
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: clarify PyPI availability timing in docs
- README: note source install is useful when PyPI version lags
- Installation guide: explain PyPI follows GitHub releases and may
lag briefly; source installs are always immediately available
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: quote version specifier in release notes install command
uv tool install accepts PEP 508 specifiers when quoted. Add quotes
around 'specify-cli==VERSION' so users can copy-paste directly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: use specify-cli@latest consistently
Use @latest to force a fresh PyPI resolve (bypasses uv's cached tool
version), matching the issue acceptance criteria. Source install remains
as fallback when PyPI lags.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: pin release notes to exact version, clarify manual publish
Release notes (versioned changelog) must always reference the specific
release version, not @latest. Use 'specify-cli==VERSION' for
reproducibility.
Also clarify that PyPI publishing is 'performed after' (not 'follows')
each release, making the manual nature clearer.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: keep source install as primary, PyPI as alternative
Until PyPI ownership is fully transferred and first publish is
confirmed, source installs from GitHub remain the primary recommended
method. PyPI install is listed as a convenient alternative.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: align checkout pin, soften PyPI wording, absolute links
- Align actions/checkout to v7.0.0 (same SHA as test.yml/release.yml)
- Remove assertion that PyPI is published by maintainers (ownership
transfer still pending); keep as availability statement
- Use 'once published for this release' wording in release notes
- Convert remaining relative links in README to absolute URLs for
PyPI rendering
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: align docs and release notes with pre-transfer state
- docs/installation.md: qualify PyPI as available 'once official
publishing is enabled' (ownership transfer still pending)
- release.yml: use specify-cli@VERSION syntax (consistent with
README/docs @latest form)
- PR description updated to match
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: revert release notes to match main
The release.yml release notes template should not change in this PR.
PyPI install instructions can be added to release notes in a future
PR once publishing is confirmed working.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: revert README and installation docs to match main
Do not mention PyPI in documentation until the first official PyPI
release has been published. This PR only adds the workflow and readme
metadata in pyproject.toml.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: fail fast if build produces no artifacts
Add if-no-files-found: error to upload-artifact so a missing/empty
dist/ directory fails the build job immediately rather than causing
a confusing failure in the publish job.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: align artifact action pins with repo lockfiles
Update upload-artifact to v7.0.1 and download-artifact to v8.0.1,
matching the pins used in the repo's gh-aw workflow lockfiles.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor: move extension command handlers to extensions/_commands.py (PR-7/8)
Convert the flat extensions.py module into an extensions/ package and
extract all extension_app and catalog_app command handlers plus their
private helpers (_resolve_installed_extension, _resolve_catalog_extension,
_print_extension_info) out of __init__.py into the new
extensions/_commands.py, mirroring the domain-dir layout used for
presets/_commands.py (PR-6) and integrations/_commands.py (PR-5).
- extensions.py -> extensions/__init__.py (pure rename, 99%); intra-module
relative imports bumped from `.x` to `..x` since they reference root
siblings.
- Root helpers (_require_specify_project, _locate_bundled_extension,
load_init_options, _display_project_path) are reached through thin shims
that re-fetch from the parent package at call time, so test
monkeypatching of specify_cli.<helper> keeps working unchanged.
- __init__.py drops ~1444 lines (3511 -> 2067); CLI surface preserved via
register(app).
No behavior change. Full suite failure set is identical before/after
(82 pre-existing env failures, 0 new).
* fix(extensions): preserve per-command path in update backup for skills agents
Skills agents (extension == "/SKILL.md") name every command file SKILL.md,
each in its own per-command subdir (e.g. speckit-plan/SKILL.md). The update
backup keyed the backup path on cmd_file.name alone, so all of an agent's
skill files collided onto a single backup path — each shutil.copy2 overwrote
the previous one, and rollback restored one skill's content over all the
others, corrupting or losing the rest.
Mirror the real on-disk layout by using cmd_file.relative_to(commands_dir),
keeping each backup path unique. This also makes backed_up_command_files
values unique so restore copies the correct content back to each command.
Add a regression test asserting two distinct skill files survive a
backup -> failed-update -> rollback cycle with their own content.
* style(extensions): use yaml.safe_dump when writing catalog config
The catalog add/remove handlers wrote the integration catalog config with
yaml.dump. Switch to yaml.safe_dump to align with the SafeDumper used by the
presets commands and to refuse emitting !!python/object tags if a non-basic
value ever reaches the config dict.
Output is unchanged for the current basic-type payload (str/int/bool/dict/
list) — this is a defensive/consistency change, not a behavioral fix.
* fix(extensions): correct _print_cli_warning import path in skill registration
register_enabled_extensions_for_agent imported _print_cli_warning from `.` (the extensions package), but the helper lives in the parent specify_cli package. The wrong level raised ImportError inside the error handlers, aborting extension/skill registration on the first failure instead of warning and continuing. Use `..` to match the other parent-package imports.
* fix(extensions): escape untrusted values in Rich markup output
User-provided arguments and extension/catalog metadata (names, descriptions, versions, IDs, paths) were interpolated into Rich markup strings without escaping. Values containing markup sequences (e.g. [red]...) would be parsed as markup, allowing output injection that could corrupt or mislead CLI messages.
Wrap all such interpolations with rich.markup.escape across the extension/catalog command handlers: list, search, info (_print_extension_info), add (including --dev paths), remove, enable, disable, set-priority, update, and the ambiguous-match resolvers (error strings and Table rows). Reuse the already-computed safe_extension where available.
Escaping is a no-op for benign strings, so normal output is unchanged.
* Prevent Rich markup injection in extension CLI output
User-controlled catalog URLs and extension IDs are rendered through Rich-enabled console paths, so every remaining output-only interpolation now escapes markup while leaving stored values and filesystem behavior unchanged. Regression tests cover catalog add, install hints, remove hints, and state command messages with bracketed markup-like values.
* Prevent markup injection from exception text
Rich markup remains enabled for styled CLI messages, so exception text and config path labels must be escaped before rendering. YAML parser errors, URL validation failures, download errors, and extension validation errors can include user-controlled catalog or manifest values.
Constraint: Preserve existing exception handling and user-facing error paths
Rejected: Disable Rich markup for these messages | existing output intentionally uses markup for labels and styling
Confidence: high
Scope-risk: narrow
Directive: Escape user-controlled exception text before interpolating into Rich-rendered strings
Tested: .venv/bin/python -m pytest tests/test_extensions.py -q
Co-authored-by: OmX <omx@oh-my-codex.dev>
* Prevent path and manifest review regressions
Catalog path labels are rendered through Rich markup and downloaded update manifests are trusted long enough to validate extension IDs. Escape displayed project paths before rendering, and reject non-mapping extension.yml payloads before ID validation so bad archives fail with a clear rollback reason.
---------
Co-authored-by: OmX <omx@oh-my-codex.dev>
* feat: add ZCode (Z.AI) integration
Add a skills-based integration for ZCode, Z.AI's Claude-Code-style
agent. ZCode uses the same SKILL.md layout as Claude Code, so spec-kit
installs workflows into .zcode/skills/speckit-<name>/SKILL.md, invoked
in chat as $speckit-<name>.
- ZcodeIntegration(SkillsIntegration) with .zcode/ folder and --skills option
- Register in INTEGRATION_REGISTRY
- Catalog entry (tags: cli, skills, z-ai)
- Tests via SkillsIntegrationTests mixin
- Document in integrations reference and README
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: render $speckit-* invocations for ZCode skills
ZCode is documented as a skills agent invoked with $speckit-<command>,
but the central invocation rendering only special-cased codex, so
specify init Next Steps and extension hooks rendered the dotted
/speckit.<command> form instead.
Centralize the $speckit-* decision in a DOLLAR_SKILLS_AGENTS set with an
is_dollar_skills_agent() helper, and route both init Next Steps and
HookExecutor._render_hook_invocation through it. Add ZCode invocation
regression tests mirroring the existing Codex/Kimi coverage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(presets): use _repo_root() for bundled-core source-checkout fallback
The tier-5 fallback in PresetResolver.resolve() and
_find_bundled_core() computed the repo root as
Path(__file__).parent.parent.parent. After presets.py was moved to
presets/__init__.py (#2826) that chain is one level short, resolving
to src/ and looking for src/templates/commands/<cmd>.md, which never
exists. As a result, wrap-strategy presets found no core base layer in
source/editable installs.
Use the shared _repo_root() helper so both fallbacks resolve against
the actual repo-root templates/ tree. Wheel installs were unaffected
(core_pack path), so this only impacts source/editable checkouts.
Refs #3086
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(presets): restore dropped def for oserror-manifest test
A prior edit accidentally removed the
def test_resolve_extension_command_via_manifest_skips_oserror_manifests
line, orphaning its body inside the new bundled-core test. Restore the
test definition so pytest collects it again.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(presets): move bundled-core tests into TestPresetResolver
The two tier-5 fallback regression tests exercise collect_all_layers()
and resolve(), not resolve_core(), so they belong in TestPresetResolver
rather than TestResolveCore. Relocate them for clearer suite navigation.
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.11.4
* chore: begin 0.11.5.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Add Tasks to GitHub Project extension to community catalog
Add tasks-to-project extension submitted by @mancioshell to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table
Closes#3082
Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Revert catalog re-serialization churn and drop git tool requirement
Restore extensions/catalog.community.json to upstream content and add only
the tasks-to-project entry, removing the unrelated Unicode-escape and
tool-object expansion churn across the catalog. Drop the git tool from the
entry's requirements to match the published extension.yml (gh + python3).
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: fail loudly on an unknown workflow expression filter
The expression evaluator's filter dispatch fell through to `return value`
for any unregistered filter, so a typo'd or unsupported filter such as
`{{ items | length }}` rendered the value unchanged with no error and the
run completed — a silent wrong result.
Raise a clear ValueError instead, naming the offending filter and the valid
ones, mirroring the strict handling already used for `from_json`. The five
registered filters (default/join/map/contains/from_json) are unchanged; the
`name(arg)` form of an unknown filter is now caught too.
* fix: distinguish a misused registered filter from an unknown one; cover map
Address the review feedback on the unknown-filter fail-loud path:
- A *registered* filter used in an unsupported form (e.g. `| join` or
`| map` with no argument) raised the misleading "unknown filter
'<name>'" — the filter is registered, the syntax isn't. It now raises
a message naming it as a known filter misused. A new
`_REGISTERED_FILTERS` constant drives the distinction.
- `test_registered_filters_unaffected` now also exercises `map('attr')`,
which it previously claimed to cover but didn't. Add
`test_registered_filter_unsupported_form_raises` to pin the new path.
* fix: include the no-arg default form in the filter-error hint
Copilot review: the hint listed default('x') but omitted the valid
no-argument default form (| default), which this module supports.
The unanchored `lib/` pattern matched any nested `lib/` directory, including
`src/specify_cli/bundler/lib/` added in #3070. Hatchling uses .gitignore as
its file-exclusion filter, so the bundler subpackage was silently dropped from
wheels built via `uvx --from git+...`, causing:
ModuleNotFoundError: No module named 'specify_cli.bundler.lib'
Prefixing with `/` anchors both patterns to the repository root, which is the
intended scope (exclude top-level lib/ artefacts from old-style setuptools
installs) without affecting nested source packages.
* fix(build): include specify_cli.bundler.lib in built distribution
The root .gitignore carried unanchored `lib/` and `lib64/` patterns from the
standard GitHub Python template (intended to ignore a top-level build/venv
`lib` directory). Being unanchored, they also match the source package
`src/specify_cli/bundler/lib/`.
Hatchling applies .gitignore patterns as build-exclusion rules, so the
`bundler/lib` package (project.py, versioning.py, yamlio.py) was silently
dropped from the built wheel even though it is tracked in git. Since
commands/bundle/__init__.py imports `specify_cli.bundler.lib.project` at module
load, any install built from source (e.g. `uv tool install --from git+...`)
crashed on startup with:
ModuleNotFoundError: No module named 'specify_cli.bundler.lib'
which broke the entire CLI — every command, including `specify init`.
Anchor the patterns to the repo root (`/lib/`, `/lib64/`) so they only match
the intended top-level build artifacts and no longer exclude the source package.
* ci: retrigger checks
Empty commit to re-dispatch a wedged CodeQL run that never started,
unblocking code scanning merge protection.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: validate command 'file' field against path traversal in registrar
CommandRegistrar.register_commands() read each command body from
source_dir / cmd_file without validating the manifest 'file' field,
unlike the parallel skill and preset readers which already reject
absolute paths and '..' traversal. A malicious extension/preset/bundle
manifest with file: ../../../etc/passwd (or an absolute path) could
read arbitrary host files verbatim into a generated agent command at a
predictable path (GHSA-w5fv-7w9x-7fc5, CWE-22).
Add the same containment guard at the command read site and reject a
traversal/absolute 'file' at manifest-load time in
ExtensionManifest._validate() for defense-in-depth, plus regression
tests for both the read path and the manifest validator.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test/fix: address review — robust absolute-path test and tolerant reads
- register_commands(): use is_file() instead of exists() and skip the
command if read_text() raises (directory or non-UTF8 file), aligning
with the other command/skill readers.
- Traversal tests: point the absolute-path payload at the real temp
secret.txt (guaranteed to exist on all platforms) instead of
/etc/passwd, so the absolute-path guard is genuinely exercised and the
test fails if it regresses, rather than passing because the target
happens not to exist (e.g. on Windows runners).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: rename traversal fixtures to avoid CodeQL secret-storage false positive
The regression fixtures named an out-of-tree file secret.txt with
TOP-SECRET-CREDENTIAL content. CodeQL's clear-text-storage heuristic
treated that read content as sensitive and followed the static path
into the pre-existing write_text sinks in _write_registered_output,
raising false 'clear-text storage of sensitive information' alerts on
PR 3088. Rename the fixtures to neutral outside.txt / OUTSIDE-FILE-MARKER
and drop /etc/passwd payloads; the test semantics (a file outside
source_dir must never be read into a generated command) are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: reject Windows drive-relative 'file' values in traversal guards
is_absolute() is False for Windows drive-relative paths like C:outside.txt,
which contain no '..' yet resolve against the process CWD on that drive —
bypassing the containment guard on Windows. Evaluate the 'file' value under
PureWindowsPath as well so both the registrar runtime guard and the
manifest-load validator reject drive letters (and backslash '..' segments)
cross-platform. Extend the regression tests with drive-relative cases.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: use anchor under both path flavors so POSIX-absolute is rejected on Windows
On a Windows runner WindowsPath('/abs/outside.md').is_absolute() is False
(no drive), so the prior native-Path check let a leading-slash 'file' value
through and the manifest validator did not raise. Evaluate the value under
both PurePosixPath and PureWindowsPath and reject any non-empty anchor —
covering POSIX-absolute, Windows drive-relative, Windows absolute, and
rooted-without-drive — in both the registrar guard and the manifest
validator. The registrar join now uses the raw 'file' string so native
separators are handled by the resolve()/relative_to() containment check.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: validate command 'file' field against path traversal in registrar
CommandRegistrar.register_commands() read each command body from
source_dir / cmd_file without validating the manifest 'file' field,
unlike the parallel skill and preset readers which already reject
absolute paths and '..' traversal. A malicious extension/preset/bundle
manifest with file: ../../../etc/passwd (or an absolute path) could
read arbitrary host files verbatim into a generated agent command at a
predictable path (GHSA-w5fv-7w9x-7fc5, CWE-22).
Add the same containment guard at the command read site and reject a
traversal/absolute 'file' at manifest-load time in
ExtensionManifest._validate() for defense-in-depth, plus regression
tests for both the read path and the manifest validator.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test/fix: address review — robust absolute-path test and tolerant reads
- register_commands(): use is_file() instead of exists() and skip the
command if read_text() raises (directory or non-UTF8 file), aligning
with the other command/skill readers.
- Traversal tests: point the absolute-path payload at the real temp
secret.txt (guaranteed to exist on all platforms) instead of
/etc/passwd, so the absolute-path guard is genuinely exercised and the
test fails if it regresses, rather than passing because the target
happens not to exist (e.g. on Windows runners).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: rename traversal fixtures to avoid CodeQL secret-storage false positive
The regression fixtures named an out-of-tree file secret.txt with
TOP-SECRET-CREDENTIAL content. CodeQL's clear-text-storage heuristic
treated that read content as sensitive and followed the static path
into the pre-existing write_text sinks in _write_registered_output,
raising false 'clear-text storage of sensitive information' alerts on
PR 3088. Rename the fixtures to neutral outside.txt / OUTSIDE-FILE-MARKER
and drop /etc/passwd payloads; the test semantics (a file outside
source_dir must never be read into a generated command) are unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: reject Windows drive-relative 'file' values in traversal guards
is_absolute() is False for Windows drive-relative paths like C:outside.txt,
which contain no '..' yet resolve against the process CWD on that drive —
bypassing the containment guard on Windows. Evaluate the 'file' value under
PureWindowsPath as well so both the registrar runtime guard and the
manifest-load validator reject drive letters (and backslash '..' segments)
cross-platform. Extend the regression tests with drive-relative cases.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: use anchor under both path flavors so POSIX-absolute is rejected on Windows
On a Windows runner WindowsPath('/abs/outside.md').is_absolute() is False
(no drive), so the prior native-Path check let a leading-slash 'file' value
through and the manifest validator did not raise. Evaluate the value under
both PurePosixPath and PureWindowsPath and reject any non-empty anchor —
covering POSIX-absolute, Windows drive-relative, Windows absolute, and
rooted-without-drive — in both the registrar guard and the manifest
validator. The registrar join now uses the raw 'file' string so native
separators are handled by the resolve()/relative_to() containment check.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor: harden register_commands inputs and tighten manifest 'file' validation
Address review feedback on #3088:
- register_commands(): skip non-string/empty 'file' values instead of
raising TypeError, and hoist source_dir.resolve() out of the per-command
loop.
- ExtensionManifest._validate(): reject 'file' values with leading/trailing
whitespace with a clear ValidationError instead of a confusing
missing-file failure later.
- tests: add non-string 'file' and whitespace cases; use yaml.safe_dump
with explicit utf-8 encoding in the manifest validation test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor: align runtime '..' policy, correct comment, dedupe test helper
Address review feedback on #3088:
- register_commands(): also reject '..' segments under both POSIX and
Windows semantics, keeping runtime policy consistent with
ExtensionManifest._validate() and the skill/preset readers (not just
relying on the resolve()/relative_to() containment backstop).
- Replace the version-dependent is_absolute() claim in the extensions.py
comment with the actual portability rationale (native Path is OS-
dependent; C:foo is anchored but not absolute).
- Extract the duplicated leak-detection assertion into
_assert_no_marker_leak() and add an in-bounds '..' payload that
exercises the new runtime '..' rejection.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Extract shared path-safety policy and warn on unreadable command files
Introduce relative_extension_path_violation() in _utils.py as the single
source of truth for the extension-relative `file` path-safety policy, and
use it from both the runtime registrar guard (agents.py) and the
manifest-load validator (extensions.py) so the two cannot drift.
Warn (instead of silently skipping) when an in-bounds command file exists
but cannot be read/decoded, surfacing misconfigured extensions.
Add unit tests for the shared helper, a read-skip warning test, and make
the in-bounds `..` test create its target file so the skip is attributable
to the `..` rejection rather than file absence.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Retrigger CI
Empty commit to re-trigger code scanning / CodeQL analysis on the PR
merge ref.
Assisted-by: GitHub Copilot CLI (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(presets): preserve argument-hint in preset SKILL.md generation
Preset-provided and extension-override commands that declare
`argument-hint:` in their frontmatter had it dropped from the generated
Claude SKILL.md, and it was re-dropped when a preset was removed and its
overridden skill restored. This is the preset-side analog of the
extension fix in #2903 / #2916.
Factor the argument-hint carry-over into a shared
CommandRegistrar.apply_argument_hint() helper and apply it at the four
preset skill-generation sites (register, reconcile override-restore, and
the core/extension unregister-restore paths). The extension path from
The helper writes argument-hint into the frontmatter dict before
serialization (so a folded multi-line description cannot be split into
invalid YAML) and only for integrations that support it (those exposing
inject_argument_hint -- currently Claude), leaving build_skill_frontmatter's
shared shape unchanged for every other agent. Core templates carry no
argument-hint, so the core-restore path is a no-op. No behavior change for
non-Claude agents or the core path.
Add regression tests covering a folding description (Claude) and the
non-Claude gate (codex).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(presets): address review - guard skill_frontmatter type and tighten apply_argument_hint annotations
Add a symmetric isinstance(skill_frontmatter, dict) guard so the helper stays a safe no-op if a caller passes a non-dict, and annotate the parameters as Dict[str, Any] with an optional integration to match real call-site usage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: surface gate detail in the workflow run/resume --json payload
A paused run was indistinguishable from any other pause in the
machine-readable outcome, and the gate's prompt/options/choice never
left the human-facing stream. Record each step's type in the run
state's step results (one engine line) and, when the run sits at a
gate, add a gate block (step_id/message/options/choice) to the payload
so orchestrators can drive review gates without parsing stdout.
Reference implementation for the proposal in #2964.
Addresses #2964
* fix(workflow): only surface gate detail in --json when the run is paused
Address review (#2965): _gate_outcome() emitted a gate block whenever current_step_id pointed at a gate step. Since RunState.current_step_id is never cleared on completion, a completed/failed run whose last step was a gate leaked stale gate detail in run/resume/status --json. Guard on status == paused. Also assert CLI success in the _run_json test helper before JSON-parsing, and add direct coverage for the suppression guard.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(workflows): surface gate block on aborted runs; stabilize message
Address Copilot review:
- `_gate_outcome` now also surfaces the gate block when a run is `aborted`
by a gate rejection (`on_reject: abort`), not only when `paused`. Abort
is the only path that sets ABORTED and it leaves current_step_id on the
gate, so an orchestrator can read the recorded `choice` for the stop.
- Coerce `message` to a string (it may be a non-string YAML literal that
GateStep only coerces for interpolation) so the JSON schema stays stable.
- Tests: add a CLI-level aborted-path test, a message-coercion test, and
extend the suppression test to allow `aborted`; share the run helper via
`_invoke_json` to avoid duplicating the invoke boilerplate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(workflows): assert clean exit in gate-abort JSON test
Address Copilot review: the gate-abort test parsed stdout without first
asserting the CLI exited cleanly, so an invoke failure would surface as an
opaque JSON decode error. Route it through `_run_json` (which asserts
exit_code == 0 before parsing) and drop the now-redundant `_invoke_json`
helper — a gate abort emits the payload and returns, so the run exits 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: use result.output in run-helper assert; document step_data shape
Address Copilot review:
- `_run_json` asserted with `result.stdout` in the message, but under
`--json` step output is redirected off stdout — the useful diagnostics
live on `result.output`. Switch the assertion message to `result.output`
(the JSON parse still reads stdout), matching the other CLI tests.
- `StepContext.steps` documented a 5-key entry shape; the engine now also
persists `type` and `status`. Update the docstring to the canonical
7-key shape so step authors/debuggers see the real record.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(workflows): align gate-abort JSON test with aborted→exit-1
After rebasing onto main, a gate abort now emits the --json payload and
then exits non-zero (`_run_outcome_exit_code` maps aborted → 1, from the
merged exit-code work). Give `_run_json` an `expected_exit` parameter
(default 0) so the abort case asserts exit 1 while the paused/completed
cases stay at 0 — keeping a single shared helper rather than duplicating
the invoke boilerplate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): backward-compat gate detection + normalize gate options
Address Copilot review:
- A run paused by an older version has no persisted step `type`, so
`_gate_outcome` would never surface its gate block on resume. Add
`_is_gate_step`: prefer the `type` field, but when it is absent fall back
to the gate's unique output signature (`on_reject`, written only by
GateStep). A record with a different known `type` is still not a gate.
- Normalize `options` to a list of strings (mirroring the `message`
coercion) so an unvalidated workflow with non-string options can't
destabilize the JSON schema.
- Tests: options coercion, type-less gate detection, and a type-less
non-gate negative case.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): normalize non-list gate options to a stable list[str]
Address Copilot review: the prior options normalization only mapped a
`list`, returning the raw value for any other shape (scalar/tuple), which
contradicted the "stable list[str]" intent. Extract `_normalize_gate_options`:
None stays None; list/tuple maps each element through str; any other scalar
becomes a single-element list (a bare string is one option, never iterated
character-by-character). The emitted schema is now always list[str] | None.
Extend the options test to cover list, tuple, bare string, numeric scalar,
and None.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): normalize gate choice to str; portable plain-gate test
Address Copilot review:
- `_gate_outcome` normalized `message` and `options` but passed `choice`
through as-is; an unvalidated gate can record a non-string `choice`,
which contradicts the stable-schema rationale. Coerce `choice` to
`str | None` (None still means "no decision yet"), consistent with the
other two fields. Adds a focused choice-coercion test.
- The plain (no-gate) test workflow used `run: "true"`, which fails under
cmd.exe on Windows (ShellStep uses shell=True). Use the cross-platform
`run: "exit 0"` (matching the exit-code suite's workflows).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* docs: dogfood Spec Kit — bundler SDD artifacts + constitution
Scaffold Spec Kit (--integration copilot) and run the full SDD workflow
against the `specify bundle` subcommand feature:
- spec.md (4 user stories, 31 FRs, 8 success criteria) + clarifications
- plan.md, research.md, data-model.md, contracts/, quickstart.md
- tasks.md (43 dependency-ordered tasks, organized by user story)
- Spec Kit Constitution v1.0.0 (code quality, testing, UX, performance,
dependency/security principles) derived from deep codebase analysis
- plan Constitution Check + tasks grounded against the ratified principles
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(bundler): add `specify bundle` subcommand for role-based setups
Implements the Spec Kit Bundler as a `specify bundle ...` subcommand group
that calls existing primitive machinery in-process with zero new dependencies,
per the v1.0.0 constitution (Principles I-V).
Adds the `specify_cli.bundler` package (models, services, lib helpers) and the
`commands/bundle` Typer group wiring search, info, list, install, update,
remove, validate, build, init, and catalog list/add/remove (with --json and
--offline). Includes manifest/catalog schemas, version + integration-clash
gating, discovery-only refusal, idempotent install with atomic rollback,
non-collateral removal, and offline-first catalog resolution.
Ships an 82-test suite (contract/unit/integration), four sample role bundles
(product-manager, business-analyst, security-researcher, developer), README
"Bundles" docs, and an AGENTS.md pitfall on the test-venv gotcha. Marks
tasks T001-T043 complete and records follow-ups T044 (live in-process
primitive dispatch) and T045 (install from a local artifact path).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(contributing): document running the full test suite via project .venv
Add a "Running the full test suite" subsection under Automated checks covering
`uv pip install -e ".[test]"` + `.venv/bin/python -m pytest`, with the
shared/global editable-install contamination caveat that mirrors the AGENTS.md
pitfall.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(bundler): wire real in-process primitive install + local-artifact install
Closes the two follow-ups left after the initial bundler landing.
T044 — DefaultPrimitiveInstaller now performs real installs through existing
machinery instead of raising "use the primitive command" errors:
- presets/extensions install via their reusable managers
(install_from_directory / install_from_zip); bundled assets install fully
offline, catalog assets are fetched only when the network is allowed.
- workflows/steps delegate to the existing `workflow add` / `workflow step add`
command callables in-process (project root as cwd), avoiding any duplicated
download/validation logic (Principle I).
- `--offline` is threaded through DefaultPrimitiveInstaller(allow_network=…) so
network-only kinds refuse with an actionable message rather than silently
reaching out.
T045 — `specify bundle install` now accepts a local path (a built .zip
artifact, a bundle directory, or a bundle.yml) and installs directly without
consulting the catalog stack; bundle-ids still resolve via the stack.
Adds 13 tests (routing, offline gating, local-source resolution, and an
end-to-end offline build → install → list → remove of the bundled
agent-context extension). Bundler suite: 95 passing; ruff clean. Marks T044
and T045 complete in tasks.md.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(bundler): append Phase 8 convergence tasks from converge assessment
Ran the converge command: assessed the codebase against spec.md, plan.md,
tasks.md, and the v1.0.0 constitution. Appended 7 traceable gap-closure tasks
(T046–T052) as a new "Phase 8: Convergence" section. Append-only — no existing
tasks were modified and no application code was changed.
Findings: 1 CRITICAL (Constitution III — bundle group undocumented under
docs/reference/), 3 HIGH (FR-005/SC-007 validate references; FR-009/SC-002 info
expansion; FR-012 install-time init), 3 MEDIUM (FR-013 integration precedence;
FR-020 surface overlaps; FR-028 update refresh).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Implement Phase 8 convergence tasks (T046–T052)
Close the gaps the converge command found between the bundler spec/plan/
constitution and the code:
- T046: add docs/reference/bundles.md documenting the full `specify bundle`
command group; link it from docs/reference/overview.md (Constitution III).
- T047: wire a reference checker into `bundle validate` (services/references.py);
online runs fail and name unresolved component references, offline runs warn.
- T048: expand `bundle info` to enumerate the full component set (versions,
preset priority/strategy) plus the bundle integration — info == install.
- T049/T050: `bundle install`/`bundle init` now scaffold an uninitialized
project via the existing `specify init` machinery, choosing the integration by
precedence (override → bundle-declared → Copilot + OS default script type).
- T051: surface foreseeable component overlaps during info and install.
- T052: `bundle update` refreshes already-installed components via a new
refresh path in install_bundle, preserving primitive-level overrides.
Adds unit/contract/integration coverage (107 tests pass).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* converge: append Phase 9 (T053) — surface bundle trust indicator
Re-run of converge after Phase 8. The seven Phase 8 tasks are verified closed.
One residual partial gap remains: the `verified`/trust indicator (FR-010,
FR-027) is exposed only in `bundle info --json`, absent from `bundle search`
(the primary discovery surface) and `bundle info` text. Appended as a single
new task for implement to complete. Append-only; no code changed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Implement T053 — surface bundle trust indicator in discovery
`bundle search` (text + JSON) and `bundle info` (text + JSON) now expose each
catalog entry's verification/trust level (verified vs community), so users can
judge a bundle's trust before installing, per FR-010 / FR-027. Previously
`verified` was only present in `bundle info --json`.
Adds contract coverage; 108 tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: dogfood Spec Kit — bundler SDD artifacts + constitution
Scaffold Spec Kit (--integration copilot) and run the full SDD workflow
against the `specify bundle` subcommand feature:
- spec.md (4 user stories, 31 FRs, 8 success criteria) + clarifications
- plan.md, research.md, data-model.md, contracts/, quickstart.md
- tasks.md (43 dependency-ordered tasks, organized by user story)
- Spec Kit Constitution v1.0.0 (code quality, testing, UX, performance,
dependency/security principles) derived from deep codebase analysis
- plan Constitution Check + tasks grounded against the ratified principles
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(bundler): add `specify bundle` subcommand for role-based setups
Implements the Spec Kit Bundler as a `specify bundle ...` subcommand group
that calls existing primitive machinery in-process with zero new dependencies,
per the v1.0.0 constitution (Principles I-V).
Adds the `specify_cli.bundler` package (models, services, lib helpers) and the
`commands/bundle` Typer group wiring search, info, list, install, update,
remove, validate, build, init, and catalog list/add/remove (with --json and
--offline). Includes manifest/catalog schemas, version + integration-clash
gating, discovery-only refusal, idempotent install with atomic rollback,
non-collateral removal, and offline-first catalog resolution.
Ships an 82-test suite (contract/unit/integration), four sample role bundles
(product-manager, business-analyst, security-researcher, developer), README
"Bundles" docs, and an AGENTS.md pitfall on the test-venv gotcha. Marks
tasks T001-T043 complete and records follow-ups T044 (live in-process
primitive dispatch) and T045 (install from a local artifact path).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(contributing): document running the full test suite via project .venv
Add a "Running the full test suite" subsection under Automated checks covering
`uv pip install -e ".[test]"` + `.venv/bin/python -m pytest`, with the
shared/global editable-install contamination caveat that mirrors the AGENTS.md
pitfall.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(bundler): wire real in-process primitive install + local-artifact install
Closes the two follow-ups left after the initial bundler landing.
T044 — DefaultPrimitiveInstaller now performs real installs through existing
machinery instead of raising "use the primitive command" errors:
- presets/extensions install via their reusable managers
(install_from_directory / install_from_zip); bundled assets install fully
offline, catalog assets are fetched only when the network is allowed.
- workflows/steps delegate to the existing `workflow add` / `workflow step add`
command callables in-process (project root as cwd), avoiding any duplicated
download/validation logic (Principle I).
- `--offline` is threaded through DefaultPrimitiveInstaller(allow_network=…) so
network-only kinds refuse with an actionable message rather than silently
reaching out.
T045 — `specify bundle install` now accepts a local path (a built .zip
artifact, a bundle directory, or a bundle.yml) and installs directly without
consulting the catalog stack; bundle-ids still resolve via the stack.
Adds 13 tests (routing, offline gating, local-source resolution, and an
end-to-end offline build → install → list → remove of the bundled
agent-context extension). Bundler suite: 95 passing; ruff clean. Marks T044
and T045 complete in tasks.md.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs(bundler): append Phase 8 convergence tasks from converge assessment
Ran the converge command: assessed the codebase against spec.md, plan.md,
tasks.md, and the v1.0.0 constitution. Appended 7 traceable gap-closure tasks
(T046–T052) as a new "Phase 8: Convergence" section. Append-only — no existing
tasks were modified and no application code was changed.
Findings: 1 CRITICAL (Constitution III — bundle group undocumented under
docs/reference/), 3 HIGH (FR-005/SC-007 validate references; FR-009/SC-002 info
expansion; FR-012 install-time init), 3 MEDIUM (FR-013 integration precedence;
FR-020 surface overlaps; FR-028 update refresh).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Implement Phase 8 convergence tasks (T046–T052)
Close the gaps the converge command found between the bundler spec/plan/
constitution and the code:
- T046: add docs/reference/bundles.md documenting the full `specify bundle`
command group; link it from docs/reference/overview.md (Constitution III).
- T047: wire a reference checker into `bundle validate` (services/references.py);
online runs fail and name unresolved component references, offline runs warn.
- T048: expand `bundle info` to enumerate the full component set (versions,
preset priority/strategy) plus the bundle integration — info == install.
- T049/T050: `bundle install`/`bundle init` now scaffold an uninitialized
project via the existing `specify init` machinery, choosing the integration by
precedence (override → bundle-declared → Copilot + OS default script type).
- T051: surface foreseeable component overlaps during info and install.
- T052: `bundle update` refreshes already-installed components via a new
refresh path in install_bundle, preserving primitive-level overrides.
Adds unit/contract/integration coverage (107 tests pass).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* converge: append Phase 9 (T053) — surface bundle trust indicator
Re-run of converge after Phase 8. The seven Phase 8 tasks are verified closed.
One residual partial gap remains: the `verified`/trust indicator (FR-010,
FR-027) is exposed only in `bundle info --json`, absent from `bundle search`
(the primary discovery surface) and `bundle info` text. Appended as a single
new task for implement to complete. Append-only; no code changed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Implement T053 — surface bundle trust indicator in discovery
`bundle search` (text + JSON) and `bundle info` (text + JSON) now expose each
catalog entry's verification/trust level (verified vs community), so users can
judge a bundle's trust before installing, per FR-010 / FR-027. Previously
`verified` was only present in `bundle info --json`.
Adds contract coverage; 108 tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): address PR review — annotations, Windows paths, HTTPS, errors, reproducible builds
Resolves automated review feedback on github/spec-kit#3070:
- validator: drop redundant string-quoting on ReferenceChecker's
`str | None` return so the annotation evaluates as a real union under
`from __future__ import annotations`.
- adapters: normalize Windows drive-letter paths (e.g. C:\...) to the
local-file branch so offline file catalogs resolve on Windows.
- adapters: enforce HTTPS (HTTP only for localhost) and require a host on
remote catalog URLs before any network call, mirroring
specify_cli.catalogs URL validation (MITM/downgrade protection).
- adapters: pass `origin` to loads_json for local files and HTTP payloads
so JSON parse errors name the real source instead of <string>.
- manifest: parse component `priority` defensively, raising an actionable
BundlerError on non-integer values instead of a raw ValueError.
- packager: write zip members with a fixed timestamp + permissions so
identical inputs yield byte-for-byte identical artifacts (genuinely
reproducible builds), and strengthen the determinism test accordingly.
Adds regression tests for priority validation, plain-HTTP/host rejection,
and byte-level artifact reproducibility (111 bundler tests pass; ruff clean).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): address PR review round 2 — nested output dir + file:// URLs
- packager: when --output points inside the bundle directory, exclude the
whole output subtree from collection so previously-built artifacts are
never re-packaged (prevents broken reproducibility and unbounded growth).
- adapters: resolve file:// catalog URLs via url2pathname and preserve
netloc, so Windows file URLs (file:///C:/...) and UNC shares
(file://server/share) resolve correctly instead of dropping the host or
producing /C:/x.
Adds regression tests for nested-output exclusion and file:// resolution
(113 bundler tests pass; ruff clean).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): address PR review round 3 — discovery UX + hardening
- bundle search/info: fall back to the built-in/user catalog stack instead of
requiring a Spec Kit project, so discovery works in a fresh directory (and
the README/quickstart examples now match actual behavior). install still
auto-initializes a project as before.
- packager: traverse with os.walk(followlinks=False) and prune symlinked
directories before descending, so a symlink-to-dir can no longer pull in
out-of-tree files (which previously turned "skip symlinks" into a hard
ensure_within() failure and did extra filesystem work).
- records: parse contributed-component priority defensively, raising an
actionable BundlerError on a corrupt records file instead of leaking a raw
ValueError/traceback.
- installer: give install_bundle's manifest parameter an explicit
BundleManifest | None type for a clearer, safer service API.
Adds regression tests for project-less search/info, symlinked-dir pruning,
and corrupt-priority records (117 bundler tests pass; ruff clean).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): address PR review round 4 + markdownlint exclusions
Review fixes:
- bundle info: expand the manifest regardless of install policy so
discovery-only bundles remain inspectable (only install is refused).
- _download_manifest: handle local .zip download_url by extracting bundle.yml
(via _local_manifest_source), and add a real remote HTTPS fetch path using
the shared authenticated, redirect-validated open_url client (HTTPS enforced
on the initial URL and every redirect; offline still refuses).
- _run_init: thread the --offline flag through to the init callback so
`bundle install/init --offline` never performs network init.
- conflict.ConflictReport: use field(default_factory=list) and drop the
None + __post_init__ workaround.
- CatalogSource.from_dict: parse priority defensively, raising an actionable
BundlerError naming the source + offending value instead of a raw ValueError.
markdownlint:
- Exclude .specify/, .github/, and specs/ (and their subdirectories) from
markdownlint so the in-flight dogfooding scaffolding doesn't trip the linter.
Adds regression tests for discovery-only info, local-zip download_url, and
non-integer catalog priority (120 bundler tests pass; ruff clean; the PR's own
markdown lints clean).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): address PR review round 5 + ignore generated files in whitespace check
Review fixes:
- packager: exclude any prior build artifact for this bundle (matching
<id>-*.zip), not just the current output path, so older artifacts next to
bundle.yml are never re-packaged.
- docs(bundles): correct the note — `search` and `info` work without a project
(they fall back to the built-in/user catalog stack); only list/update/remove/
catalog require an initialized project.
CI / generated files:
- .gitattributes: mark the generated dogfooding scaffolding (.specify/**, the
speckit .github agent/prompt files, copilot-instructions.md, specs/**) with
-whitespace so `git diff --check` (the Lint workflow's whitespace gate) stops
flagging emitted trailing whitespace. These files are produced by
`specify init` and are scrubbed before merge.
Adds a regression test for prior-artifact exclusion (121 bundler tests pass;
ruff clean).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): collision-resistant catalog ids, canonical local paths, explicit uninstalled result
Addresses review round 6 (PR #3070):
- catalog_config._derive_id now combines host label with the URL path stem so
multiple catalogs from the same host get distinct, stable default ids.
- add_source canonicalizes local file paths to absolute before persisting, so
project config no longer depends on the caller's cwd.
- InstallResult gains a dedicated `uninstalled` list; remove_bundle no longer
overloads `installed` for removals, and the CLI prints from `uninstalled`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): confine config writes, guard indeterminate integration, fix validate docs
Addresses review round 7 (PR #3070):
- save_records and catalog_config._write now pass within=project_root to
dump_json/dump_yaml, refusing symlinked .specify paths that escape the
project (defense-in-depth, matching the rest of the codebase).
- resolve_install_plan now fails when a bundle pins an integration but the
project's active integration cannot be determined and no explicit
--integration override was given, instead of silently adopting the bundle's
required integration (FR-019 guard). CLI passes integration_explicit.
- docs/reference/bundles.md: corrected the validate semantics to describe the
actual best-effort online behavior (unreachable catalogs warn, not fail).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): Windows path handling + review round 8 hardening
Fix Windows CI failures:
- is_safe_relpath now rejects POSIX-absolute (/abs) and Windows drive-absolute
(C:\x, UNC) paths on every OS, instead of passing them through on Windows
where os.path.isabs('/abs') is False and Path('/abs').parts yields '\\'.
- _download_manifest treats a Windows drive-letter download_url (C:\bundle.yml,
which urlparse reads as scheme 'c') as a local file, fixing the empty
component set in `bundle info` on Windows.
Address review round 8 (PR #3070):
- Bundled workflows now install under --offline (locate via
_locate_bundled_workflow) instead of being refused unconditionally.
- bundle update preserves the original installed_at timestamp on refresh
(import find_record; reuse the existing record's timestamp).
- _derive_id lowercases the host label so 'Example.com' and 'example.com'
produce the same deterministic id.
- CatalogEntry.from_dict validates 'tags' is a list and 'verified' is a real
boolean, raising BundlerError on invalid untrusted shapes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): normalize SemVer prerelease spellings before version parsing
Addresses review round 9 (PR #3070): parse_version and is_semver now apply the
same prerelease normalization (mirroring specify_cli._version._normalize_tag)
so SemVer spellings like 1.2.3-rc1 / 1.2.3-alpha1 validate and compare
consistently across is_semver, parse_version, and satisfies. Leading 'v' is
also stripped. Keeps the manifest validator and constraint checks in agreement.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): no collateral removal + enforce manifest-pinned versions
Addresses review round 10 (PR #3070):
- install_bundle records only the components this bundle actually contributed:
freshly-installed components, plus pre-existing ones already owned by this
bundle (refresh) or a sibling bundle (shared/refcounted). A component that is
installed on disk but tracked by no bundle was installed independently and is
no longer attributed, so `bundle remove` won't uninstall it (FR-022).
- preset/extension/workflow install paths now verify the active catalog's
advertised version matches the manifest-pinned component.version before
downloading/installing, raising BundlerError on mismatch so bundles stay
reproducible. When a catalog advertises no version the pin can't be enforced
and installation proceeds.
Added regression tests: independent pre-existing component survives removal;
version-mismatch refusal (helper + workflow path).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(scripts): add SPECIFY_INIT_DIR to target a member project from the repo root (#2892)
* feat(scripts): add SPECIFY_INIT_DIR to target a member project from the repo root
Resolve an explicit SPECIFY_INIT_DIR project override once in the core
get_repo_root / Get-RepoRoot, so a non-interactive / CI caller can target a
member project (the directory containing .specify/) from a monorepo root
without cd. Strict by design: the path must exist and contain .specify/,
otherwise it hard-errors with no silent fallback.
- Single resolver in core; the git feature-branch script inherits it by
sourcing core, with no per-extension copies.
- PS resolver verifies the resolved path is a directory (Resolve-Path also
succeeds for files) so a file value errors as "not an existing directory".
- get_feature_paths splits decl/assignment so a SPECIFY_INIT_DIR failure
propagates instead of being masked by `local`.
- create-new-feature-branch: when core is absent (only git-common loaded) and
SPECIFY_INIT_DIR is set, hard-error rather than silently using the git root.
- Document SPECIFY_INIT_DIR and SPECIFY_FEATURE_DIRECTORY in the core reference.
- Tests for valid/relative/trailing-slash/file/missing/no-.specify targets,
feature-axis composition, the no-core guard, and a PowerShell mirror.
* fix: guard SPECIFY_INIT_DIR with stale core scripts
* docs: clarify SPECIFY_FEATURE_DIRECTORY precedence wording
* fix: normalize trailing slash in PowerShell SPECIFY_INIT_DIR resolver
Resolve-Path preserves a trailing separator from its input, so a
SPECIFY_INIT_DIR ending in a slash returned a root that didn't match the
bash resolver (whose `cd && pwd` strips it). That broke
test_ps_trailing_slash_tolerated on the CI runners, which do have pwsh.
Trim it with TrimEndingDirectorySeparator (no-op on a bare root or a path
with no trailing separator).
Also fix the misleading test comment: the PowerShell mirror runs on the
CI ubuntu/windows runners (they ship pwsh), it is not skipped there.
* test: normalize bash path expectations on Windows
* docs: clarify SPECIFY_INIT_DIR root helpers
* chore: sync dogfooded .specify core scripts with SPECIFY_INIT_DIR
Mirror the SPECIFY_INIT_DIR resolver (resolve_specify_init_dir in
common.sh) into the committed dogfooding .specify/scripts/bash copies so
the git extension's create-new-feature-branch.sh finds an up-to-date
common.sh instead of failing with "requires updated Spec Kit core
scripts". Fixes the test_init_dir.py CI failures.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): harden remote catalog fetch and config parsing
- adapters: route catalog HTTP fetches through the shared authenticated
client (authentication.http.open_url) so auth.json tokens apply and the
Authorization header is stripped on cross-host/downgrade redirects.
Reject any redirect that leaves HTTPS via a redirect_validator and
re-validate the final URL after redirects, closing the urlopen
auto-redirect MITM/downgrade gap.
- catalog_config._read: raise an actionable BundlerError when the config
top level is not a mapping, 'catalogs' is not a list, or an entry is
not a mapping, instead of letting list(<str>) produce a downstream
AttributeError.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): tighten record read confinement, policy gate, and precedence
Addresses review 4534504799:
- records.load_records: confine the read via ensure_within(project_root,
...) so a symlinked/traversal-escaping .specify cannot read arbitrary
files outside the project (matches the write path's within= guard).
- catalog_config._slug: lowercase so derived catalog ids are
deterministic across platforms and case-variant duplicates can't slip
past the case-sensitive dup check.
- installer.install_bundle: reword the docstring's misleading "atomic on
failure" claim to describe the real scoped guarantee (record written
only on full success; rollback limited to newly-installed components).
- bundle update: enforce the source install_policy like install, refusing
to update from a discovery-only source (FR-025).
- catalog source precedence: the CLI now passes ~/.specify as the user
config dir so project > user > built-in precedence is actually
reachable (previously the user scope was silently ignored).
- .gitattributes: scope the specs whitespace exemption to the generated
dogfooding feature dir (specs/001-spec-kit-bundler/**) instead of all
of specs/**.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): no collateral refresh, catalog id integrity, loud info
Addresses review 4534571362:
- installer: in refresh mode (bundle update) only re-apply already-
installed components that this bundle (or a sibling) owns. Components
installed independently and tracked by no bundle are now skipped, never
refreshed, so update cannot make collateral changes (FR-022).
- catalog.load_catalog_payload: validate each entry's own id is present
and matches its enclosing bundles key, rejecting catalogs that would
otherwise list a spoofed or unresolvable id.
- bundle info: stop swallowing manifest download failures. If the
manifest can't be resolved (e.g. --offline against an https download_url
or a download failure), surface the error and exit non-zero instead of
silently degrading to catalog `provides` counts, preserving the "info
== what install applies" guarantee.
Added regressions: refresh leaves independently-installed components
untouched, catalog id key/field mismatch + missing id rejection, and
info exits non-zero when the manifest is unresolvable offline.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): confine catalog-config and integration-marker reads
Addresses review 4534716790: two more state reads bypassed the
symlink/path-escape confinement that records and the write paths already
enforce.
- catalog_config._read: validate the config path with
ensure_within(project_root, ...) before exists()/read, so a symlinked
.specify resolving outside project_root is rejected instead of read.
- lib.project.active_integration: confine the .specify/integration.json
read the same way; an out-of-tree escape is treated as "not
determinable" (returns None) rather than followed.
Added regressions covering both via a symlinked .specify pointing
outside the project root.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): validate manifest tags, disambiguate derived ids by full host
Addresses review 4534768419:
- manifest.from_dict: reject a non-list `tags` (e.g. a bare string) instead
of splitting it character-by-character, matching the catalog parser and
the schema contract (tags = list of strings).
- catalog_config._derive_id: derive ids from the full host (TLD included)
so example.com and example.net no longer collide on the same id. Updated
the affected id assertions.
- CHANGELOG: call out the new `specify bundle` command group in the
unreleased section (the PR's headline user-facing feature).
- .gitattributes: clarify the specs whitespace exemption — the dogfooding
feature dir is scrubbed before merge (not retained), so it doesn't weaken
checks for kept docs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore(gitattributes): retain whitespace exemption for constitution.md
The project constitution (.specify/memory/constitution.md) is the one
dogfooding artifact carried forward past the pre-merge scrub. Give it its
own standalone whitespace exemption so it survives removal of the broader
.specify/** generated-scaffolding exemption.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): accurate uninstall count, confine catalog read, safe bundle id
Addresses review 4534812056:
- installer.remove_bundle: only count a component as uninstalled when
installer.remove() actually ran; components already absent on disk are
reported as skipped, keeping the uninstalled count accurate.
- catalog.load_source_stack: confine the project-scoped .specify config read
with ensure_within, so a symlinked .specify/ resolving outside the project
root is refused (consistent with the bundler's other guarded reads).
- manifest: enforce a filesystem-safe slug for bundle.id in structural
validation; packager.build_bundle adds an ensure_within defense-in-depth
check so a crafted id can never push the artifact outside the output dir.
Also reverts the CHANGELOG entry (the changelog is updated separately).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): validate requires/provides shapes in manifest and catalog
Addresses review 4534855443:
- manifest: validate requires.tools and requires.mcp as list-of-strings via
a shared _parse_str_list helper (also reused for tags), so a bare string
like `tools: docker` is rejected with an actionable BundlerError instead of
being split character-by-character.
- catalog.CatalogEntry.from_dict: validate that `requires` and `provides` are
mappings before accessing them, so an untrusted catalog payload with
`requires: "..."` raises a named BundlerError rather than escaping as a raw
AttributeError traceback.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): require README.md when building a bundle artifact
Addresses review 4534938014: build_bundle now fails early with an
actionable error when README.md is missing, matching the documented
artifact contract (manifest + README) instead of silently producing a
bundle with no human-facing description.
Also reverts CHANGELOG.md to the upstream/main copy.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): validate record shapes; drop stale install --refresh claim
Addresses review 4534969692:
- records.InstalledBundleRecord.from_dict: hard-error when
contributed_components is not a list, instead of iterating a corrupt
bare string character-by-character.
- records.load_records: validate the top-level 'bundles' field is a list and
fail with a clear BundlerError when a corrupt file makes it a mapping/string.
- PR description: remove the inaccurate "supports --refresh" note from
`bundle install` (refresh is the `bundle update` path); docs already omit it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): refuse symlinked .specify, reject bad url schemes, IPv6 ids
Addresses review 4534997724:
- lib.project.find_project_root: a symlinked .specify is no longer accepted
as a project root (is_dir() follows symlinks), matching the confinement the
rest of the CLI applies and avoiding confusing downstream failures.
- catalog_config.add_source: reject unsupported url schemes (ssh://, ftp://,
...) up front instead of silently treating them as local paths; local paths
containing ':' but not '://' are still allowed.
- catalog_config._derive_id: derive the host via urlparse().hostname so IPv6
literals, credentials, and ports no longer corrupt the derived id.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): strict semver, narrow artifact skip, preserve priority 0
Addresses review 4535084048:
- versioning.is_semver: enforce a full MAJOR.MINOR.PATCH SemVer (with optional
pre-release/build) via a dedicated regex, instead of accepting any
packaging.version.Version-parseable string (e.g. "1", "1.0"). This makes
BundleManifest.structural_errors() reject non-semver versions.
- packager: narrow the prior-artifact skip pattern to semver-named zips
(<id>-<x.y.z>.zip) so legitimate assets like <id>-assets.zip are still
packaged.
- primitives (preset + extension install): use an explicit `is None` check so
an intentional priority of 0 is preserved instead of being replaced by the
default.
Adds regressions: non-semver rejection ("1"/"1.0"/"1.2.3.4"), asset-not-
excluded vs semver-artifact-excluded, and priority-0 pass-through.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): artifact regex for prerelease+build; clarify integration/priority docs
Addresses review 4535132279:
- packager: the prior-artifact skip regex now matches semver names carrying
both a prerelease and build-metadata segment (e.g. 1.0.0-rc1+build5), so such
an existing artifact is excluded rather than re-packaged — keeping builds
bounded/deterministic, consistent with is_semver().
- docs/reference/bundles.md: correct the install integration wording.
--integration selects the integration when initializing a new project and
confirms the target when a pinned bundle's active integration can't be
determined; it does NOT override a bundle that targets a specific integration
(a mismatch aborts with no changes).
- examples/security-researcher README: reword the preset priority note in terms
of the numeric comparison (ascending priority order) to avoid inverting the
meaning.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): --integration can't bypass clash guard; honest rollback docs
Addresses review 4535159341:
- bundle install: for an already-initialized project, the project's recorded
active integration is now authoritative. --integration no longer overrides it
(which let a copilot project install a claude-pinned bundle via
`--integration claude`, bypassing the FR-019 clash guard). The override still
selects the integration at init time and confirms the target only when the
active integration cannot be determined.
- docs/reference/bundles.md: reword the install guarantee to match the
implementation — no provenance record is written unless the install fully
succeeds, and rollback of this run's components is best-effort (removal errors
are swallowed, so partial on-disk state may remain). Dropped the inaccurate
"atomic / rolls back everything" claim.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): validate component kind/id when loading records
Addresses review 4535194606: _component_from_dict now rejects a contributed
component whose 'kind' is not a supported component kind or whose 'id' is
empty, raising a BundlerError that explicitly flags the records file as
corrupt. Previously such a record loaded successfully and only failed later
(e.g. in primitive_manager() during bundle remove/update) with a less
actionable error.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): address review 4535234003 (7 findings)
- versioning: tolerate an uppercase `V` prefix in `_normalize_semver` and
`is_semver`, mirroring specify_cli._version tag normalization (V -> v) so
`V1.2.3` parses and validates consistently.
- validator: import BundlerError and narrow the speckit_version constraint
except clause to `BundlerError` only, so programming errors are no longer
masked behind an "invalid constraint" message.
- bundle update: accept `--integration` and thread it through
resolve_install_plan the same way `bundle install` does (override used only
when the active integration can't be auto-detected), so integration-pinned
bundles can be updated where `.specify/integration.json` is missing/unreadable.
- bundle validate: fold reference warnings into `report.warnings` so the
ValidationReport is the single warning channel at the CLI layer.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(bundler): make update --integration help assertion ANSI-safe
Rich can split the "--integration" option label with ANSI escape codes
between the two leading dashes, so the literal substring check failed under
CI's terminal settings. Match the un-split option word instead, mirroring how
test_bundle_help_lists_all_commands checks bare command names.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): preserve exec bits in artifacts; document install-time pins
Addresses review 4535280786:
- packager.build_bundle: no longer forces every ZIP member to 0644, which
stripped the executable bit from bundled scripts (e.g. extension hook
scripts) and could break them after extraction. Permissions are now
normalized reproducibly to 0755 when the source file has any execute bit
set, otherwise 0644 — identical inputs still yield byte-for-byte identical
artifacts.
- installer.install_bundle + docs/reference/bundles.md: document that version
pins are enforced install-time only. Because primitive is_installed checks
are id-based (not version-aware), an already-present component is skipped
during install without comparing its on-disk version to the manifest pin;
pins are guaranteed applied only on a real install or `bundle update` refresh.
Added a regression asserting executable sources map to 0755 and plain files to
0644 in the built artifact.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test(bundler): skip exec-bit packager test on Windows
Windows filesystems do not carry Unix execute bits, so chmod(0o755) is a no-op
and the source file reports no execute bit — the packager then correctly stores
the member as 0644. The assertion that an executable source maps to 0755 is only
meaningful on POSIX, so skip it on nt rather than asserting platform-specific
behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): normalize prerelease spellings inside version constraints
Addresses review 4535327154: parse_version() normalized SemVer prerelease
spellings (e.g. 1.2.3-rc1 -> 1.2.3rc1) but parse_constraint() passed the
constraint to packaging.SpecifierSet unmodified, so ">=1.2.3-rc1" raised
InvalidSpecifier even though the same spelling is accepted for installed
versions. parse_constraint() now normalizes the version portion of each
comma-separated clause via the shared _normalize_semver helper, so prerelease
handling is consistent across versions and constraints.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(bundler): validate schema versions and required record identity fields
Addresses review 4535351596:
- records.load_records: validate the on-disk 'schema_version' (required;
forward-compatible across same-major minor bumps) and fail fast with an
actionable error on a missing/unknown version, rather than silently parsing a
possibly-incompatible format and risking incorrect bundle attribution/removal.
- records.InstalledBundleRecord.from_dict: treat missing 'bundle_id' or
'version' as corruption and raise BundlerError, instead of coercing them to
empty strings that let later list/remove/update operations behave
unpredictably.
- catalog_config._read: validate 'schema_version' when present (same-major
compatibility) and fail fast on an unsupported version so an incompatible
future config shape can't be mis-parsed into a wrong effective catalog stack.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore(bundler): scrub generated dogfooding scaffold before merge
The bundler feature was developed by dogfooding Spec Kit on itself. Now that
the work is complete, remove all generated scaffolding so it does not land in
the repository on merge:
- specs/001-spec-kit-bundler/** (spec, plan, research, data-model, contracts,
quickstart, tasks, checklists)
- .specify/** (extensions, integrations, scripts, templates, workflows,
feature/init/integration metadata)
- .github/agents/speckit.*.agent.md, .github/prompts/speckit.*.prompt.md, and
.github/copilot-instructions.md (Copilot integration scaffold)
Retained: .specify/memory/constitution.md — the single dogfooding artifact
carried forward — with its whitespace exemption in .gitattributes.
.gitattributes and .markdownlint-cli2.jsonc are reverted to the upstream
baseline (plus the constitution whitespace exemption), dropping the now-moot
exemptions for the removed scaffold.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Pascal THUET <pascal.thuet@arte.tv>
* chore: bump version to 0.11.3
* chore: begin 0.11.4.dev0 development
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Expand the AGENTS.md PR-review section into a continuous disclosure
policy. Disclosure is no longer a one-time PR-body event:
- Commits: require an Assisted-by: (autonomous|supervised) trailer on
every agent-authored commit; ban hiding agent authorship behind the
operator's git identity; preserve tool-generated Co-authored-by lines.
- Comments: re-state agent identity each review round.
- Anti-patterns: forbid replying "Done"/pushing fixes seconds after a
review trigger without disclosure, and claiming human review for
automated commits.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: isolate per-extension failures in register_enabled_extensions_for_agent
The per-extension loop had no error isolation: if registering one enabled
extension raised (e.g. an OSError writing a command file), the loop aborted and
the exception propagated, so every subsequent enabled extension was silently
skipped. Callers wrap the whole call in a single best-effort try/except, so the
wholesale abort surfaced as one warning while the command still exited 0 —
leaving the agent with only a prefix of its extensions.
Wrap the per-extension body in try/except: warn (naming the extension) and
continue, so one bad extension can no longer drop the others. Add a regression
test that forces the first-iterated extension to raise and asserts the rest
still register.
Closes#2950
* fix(extensions): preserve command registry when skills fail
* fix: clarify skill registration warning
* fix(taskstoissues): skip tasks that already have a GitHub issue
Re-running /speckit-taskstoissues created a duplicate issue for every
task because the command never checked for existing ones. Add a
deduplication step before issue creation: list the repo's issues
(state all) via the GitHub MCP server, collect the task IDs already
present in issue titles, and skip any task that already has a matching
issue. Issue titles are now prefixed with the task ID (e.g. T001:) so
they can be matched on later runs, and list_issues is added to the
command's MCP tools.
Fixes#2968
* fix(taskstoissues): correct list_issues usage and issue title format
Address Copilot review:
- list_issues has no 'all' state; omitting state returns both open and
closed issues. Use cursor-based pagination (after/endCursor) to fetch
every page before building the dedup set.
- task lines already start with their ID, so reuse the task text as the
issue title instead of prefixing the ID again (which produced
'T001: T001 ...').
* fix(taskstoissues): match task IDs anywhere in titles and define one canonical title
Address follow-up Copilot review:
- task lines start with a markdown checkbox (- [ ] T001 ...), so the
creation step now strips the checkbox and [P]/[US#] markers and writes
a single canonical title 'T001: <description>'.
- dedup now scans each issue title for a T<digits> token anywhere in the
title, so existing issues titled 'T001 ...', 'T001: ...' or '[T001] ...'
are all matched.
* fix(taskstoissues): use word-boundary task ID match and request perPage 100
Address Copilot review:
- match issue titles against \bT\d{3}\b so tokens like ST001 or T0010
are not matched by mistake (task IDs are T + 3 digits).
- request perPage: 100 on list_issues to reduce pagination calls.
* fix(taskstoissues): bound issue pagination to the tasks being processed
Address Copilot review: extract the task IDs from tasks.md first, then
paginate list_issues only until every task ID has been matched (or pages
run out), instead of fetching the repo's entire issue history. Keeps the
call count bounded on repos with large issue backlogs.
* feat(scripts): add SPECIFY_INIT_DIR to target a member project from the repo root
Resolve an explicit SPECIFY_INIT_DIR project override once in the core
get_repo_root / Get-RepoRoot, so a non-interactive / CI caller can target a
member project (the directory containing .specify/) from a monorepo root
without cd. Strict by design: the path must exist and contain .specify/,
otherwise it hard-errors with no silent fallback.
- Single resolver in core; the git feature-branch script inherits it by
sourcing core, with no per-extension copies.
- PS resolver verifies the resolved path is a directory (Resolve-Path also
succeeds for files) so a file value errors as "not an existing directory".
- get_feature_paths splits decl/assignment so a SPECIFY_INIT_DIR failure
propagates instead of being masked by `local`.
- create-new-feature-branch: when core is absent (only git-common loaded) and
SPECIFY_INIT_DIR is set, hard-error rather than silently using the git root.
- Document SPECIFY_INIT_DIR and SPECIFY_FEATURE_DIRECTORY in the core reference.
- Tests for valid/relative/trailing-slash/file/missing/no-.specify targets,
feature-axis composition, the no-core guard, and a PowerShell mirror.
* fix: guard SPECIFY_INIT_DIR with stale core scripts
* docs: clarify SPECIFY_FEATURE_DIRECTORY precedence wording
* fix: normalize trailing slash in PowerShell SPECIFY_INIT_DIR resolver
Resolve-Path preserves a trailing separator from its input, so a
SPECIFY_INIT_DIR ending in a slash returned a root that didn't match the
bash resolver (whose `cd && pwd` strips it). That broke
test_ps_trailing_slash_tolerated on the CI runners, which do have pwsh.
Trim it with TrimEndingDirectorySeparator (no-op on a bare root or a path
with no trailing separator).
Also fix the misleading test comment: the PowerShell mirror runs on the
CI ubuntu/windows runners (they ship pwsh), it is not skipped there.
* test: normalize bash path expectations on Windows
* docs: clarify SPECIFY_INIT_DIR root helpers
* claude: run /analyze in a forked subagent
/analyze is explicitly read-only and produces a compact analysis
report from heavy artefact reads (spec.md, plan.md, tasks.md). It
matches the canonical use case for context: fork — bulk inputs that
collapse to a short summary, no need for conversation history.
Forking keeps the artefact contents out of the main conversation
context, which is the concern raised in #752.
Done as a per-command opt-in via FORK_CONTEXT_COMMANDS so other
spec-kit commands (which are interactive or have side effects) are
unaffected.
Refs #752
* claude: apply per-command frontmatter on every skill-generation path
argument-hint and fork context were injected only in setup(), so skills
produced via post_process_skill_content() directly (presets, extensions)
lost them - e.g. a preset overriding speckit-analyze dropped context: fork.
Move the per-command injection into post_process_skill_content(), deriving
the command stem from the frontmatter name, so all generation paths stay
consistent. setup() now just calls post_process_skill_content().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* claude: drop redundant post-process loop from setup
SkillsIntegration.setup() already runs post_process_skill_content()
on every SKILL.md before writing it, and that method now applies the
argument-hint and fork-context injection. The per-file re-process loop
in ClaudeIntegration.setup() was therefore a no-op, so inherit the
base setup() directly.
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.2
* chore: begin 0.11.3.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
The add-community-extension and add-community-preset agentic workflows
never ran for real submissions. Their issue templates auto-applied the
`extension-submission`/`preset-submission` label at creation, which lands
in the `opened` event (not `labeled`), and the external submitter fails
the team-membership activation gate.
Align both with the working bug-assess pattern:
- Add `names: [extension-submission]` / `[preset-submission]` so a
job-level condition gates activation on the specific label.
- Add `github: min-integrity: none` to allow reading external user issues.
- Remove the trigger label from the issue-template auto-labels so a
maintainer applies it during triage — emitting a real `labeled` event
from a team member, which passes activation.
- Recompile lock files with gh aw v0.79.8.
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The committed lock file declared compiler v0.79.8 but contained a github
allow-only guard policy with `"repos": "${GITHUB_REPOSITORY}"`. MCP Gateway
v0.3.25 rejects repo-specific values ("allow-only.repos string must be 'all'
or 'public'"), so the agent job failed at "Start MCP Gateway":
failed to register guard for server "github": invalid server guard policy:
allow-only.repos string must be 'all' or 'public'
Recompiling bug-assess.md with gh-aw v0.79.8 deterministically emits
`"repos": "all"` (the gateway-accepted default when min-integrity is set
without an explicit repos scope), confirming the committed lock was stale.
This also reconciles the manifest setup-action SHA with the value already
used in the workflow body.
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: add bug-assess agentic workflow
Add a gh-aw agentic workflow that triggers when an issue is labeled
`bug-assess`. It assesses the report against the codebase (symptom, suspected
code paths, verdict, severity, remediation) and posts the full assessment.md as
an issue comment, led by a one-line valid?/priority summary. It also applies
severity / needs-reproduction / invalid triage labels.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: disable noop report-as-issue for bug-assess workflow
Set safe-outputs.noop.report-as-issue: false so noop runs on
failures/timeouts no longer create extra report issues, keeping
outputs limited to the issue comment and triage labels.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: clarify bug-assess label filtering is job-level
Reword the Triggering Conditions paragraph to reflect that the
issues:labeled trigger fires for any label and the bug-assess
filtering happens via a job-level condition, not at the trigger.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: tighten bug-assess prompt guardrails
- Add a 65,000-char comment-size limit instruction with explicit
truncation marking so large reports don't fail the safe-outputs
validator.
- Clarify the read-only guardrail: scratch files allowed under
$RUNNER_TEMP, never write into the working tree or commit/push.
- Align the one-line summary verdict vocabulary (Invalid) with the
canonical 'invalid' verdict and Step 8 label rules.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: align bug-assess severity wording and recompile with v0.78.1
- Use 'severity' instead of 'priority' in the Step 7 one-line summary to
match Step 5, the Severity header field, and the severity-* labels.
- Clarify the read-only guardrail: comment + labels are the intended
outputs on success, while the gh-aw harness may separately emit
failure-report artifacts/issues when a run errors or times out.
- Recompile with gh-aw v0.78.1 so the gh-aw-actions/setup pin matches
the repo's other workflow lock files and actions-lock.json.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add /speckit.converge SDD artifacts and project scaffolding
Dogfood the converge feature through Spec Kit's own workflow:
- spec.md, plan.md, tasks.md, research, data-model, contracts, quickstart
- requirements checklist for the feature
- ratified constitution v1.0.0 (.specify/memory)
- Specify project scaffolding (.specify/, .github agent + prompt files)
Defines a built-in /speckit.converge command that assesses spec/plan/tasks
against the codebase and appends remaining work as new tasks (no git, no
change tracking, append-only). Implementation not yet started.
Excludes unrelated working-tree changes to agents.py, extensions.py,
test_extensions.py, catalog.community.json, and README.md.
* Implement /speckit.converge command
Add the built-in converge command that assesses the codebase against a
feature's spec.md, plan.md, and tasks.md and appends remaining unbuilt work
as new traceable tasks to tasks.md (append-only; no git, no change tracking).
- templates/commands/converge.md: full command body (load artifacts, assess
code, classify findings missing/partial/contradicts/unrequested, append
'## Phase N — Convergence' tasks with source-ref + gap-type, read-only
guardrails, converged branch, handoff, before/after_converge hooks)
- Register converge as a core command across all enumeration sites
(SKILL_DESCRIPTIONS, _FALLBACK_CORE_COMMAND_NAMES, ARGUMENT_HINTS, and the
integration test command lists incl. copilot/generic file inventories)
- init.py Next Steps panel + README Core Commands table
- tasks.md: T001-T024 complete (T025 manual quickstart pending)
Full suite green: 2343 passed.
* Record quickstart validation results for /speckit.converge (T025)
All six quickstart scenarios validated (GitHub Copilot agent, macOS/zsh):
S1 gap->appended traceable task, S2 implement+re-converge, S3 converged leaves
tasks.md unchanged, S4 read-only boundaries, S5 missing-prereq stop, S6 cross-
integration install (copilot + windsurf). Automated suite: 2343 passed.
* Record 2026-06-16 re-verification results for /speckit.converge (T025)
* 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 integration upgrade deleting settings.json and dropping script +x
Two upgrade-path bugs surfaced during converge E2E validation:
- copilot upgrade stale-deleted .vscode/settings.json because setup() only tracks the file when it creates it; on upgrade the pre-existing file is merged and left untracked, so Phase 2 stale cleanup removed it. Add an integration-level stale_cleanup_exclusions() hook (CopilotIntegration returns {.vscode/settings.json}) and subtract it from stale_keys.
- shared .specify/scripts/*.sh lost their execute bit because the managed refresh rewrites them with the bundled source mode (often 0o644) and nothing restored perms. Call ensure_executable_scripts() after the managed-refresh block (POSIX only).
Add regression tests in TestIntegrationUpgrade covering both fixes (validated to fail without the fixes).
* fix: resolve markdownlint errors in PR files
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: clean up runtime state files from PR
Remove .specify state files that are per-project runtime artifacts:
- feature.json, init-options.json, integration.json
- manifest files, extension registry, bug artifacts
These are generated by 'specify init' and should not be committed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: fold converge artifacts from #3003 and #3005
- Add speckit.converge Copilot agent and prompt files (#3003)
- Add regression test for Claude argument hints (#3005)
- Remove invalid converge entry from Claude argument hints
- Fix documentation removing branch-prefix fallback claims
Supersedes: #3003, #3005
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: remove non-converge specify scaffolding from PR
Remove .specify/ artifacts, non-converge .github/agents and prompts,
and copilot-instructions.md that were generated by 'specify init'
and are not part of the converge command feature.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: remove SDD spec artifacts from PR
Remove specs/001-converge-command/ — the spec/plan/tasks/research SDD
artifacts produced while building this feature. spec-kit does not track
a specs/ directory on main (those are outputs of running the workflow on
the repo, not part of the shipped tool).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: remove generated Copilot converge command files
Remove .github/agents/speckit.converge.agent.md and
.github/prompts/speckit.converge.prompt.md — these are generated by
'specify init --integration copilot' from templates/commands/converge.md
(all __SPECKIT_COMMAND_*__/{SCRIPT} tokens are resolved). main tracks no
.github/agents or .github/prompts files; the template is the source of truth.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: split out unrelated integration-upgrade fix
Move the stale_cleanup_exclusions / executable-bit upgrade fix
(base.py, copilot, _migrate_commands.py, test_integration_subcommand.py)
out of this PR into its own change. This PR is now scoped purely to the
/speckit.converge command.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: add converge to core command template ordering
converge is a core command in SKILL_DESCRIPTIONS but was missing from
_CORE_COMMAND_TEMPLATE_ORDER, so it sorted with the fallback rank. Add it
after 'implement' to keep core-command ordering consistent across
integrations.
Addresses review feedback on #3001.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: make converge findings example neutral
Replace the self-referential sample evidence text in the Convergence
Findings table with a neutral placeholder so agents are less likely to copy
nonsensical template-specific findings into real output.
Co-authored-by: Copilot <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>
* docs: clarify converge scope and hook outcome wording
- Remove FR-specific parenthetical from code-scope rule so it doesn't imply
a hard FR-001 reference exists in every feature
- Replace unsupported 'pass outcome to hook context' instruction with explicit
in-session outcome reporting before hook listing
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: align converge task example with tasks format
Use (no colon) in the convergence task example so it
matches tasks-template formatting and downstream expectations.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Clarification of usage
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>
* docs: align converge phase/task-id format with tasks template
- Use (colon) for consistency with tasks template
- Clarify appended task IDs must be zero-padded ( style)
- Update checklist example to a concrete zero-padded ID ()
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: standardize converge phase heading format
Use consistently in converge.md (including the
append-only contract section) to match Step 7 and tasks template style.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: preserve .vscode/settings.json and script +x bit on integration upgrade
During 'specify integration upgrade', Phase 2 stale-cleanup removes files
present in the old manifest but absent from the new one. Copilot's setup()
merges into an existing .vscode/settings.json and stops tracking it, so the
file was being deleted on upgrade (destroying user settings). Add a
stale_cleanup_exclusions() hook that integrations use to protect such
conditionally-tracked merge targets. Also restore the executable bit on
shared .sh scripts after the managed-refresh step on POSIX.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address review on stale-cleanup fix
- Normalize stale_cleanup_exclusions() to POSIX before subtracting from
manifest keys, so exclusions built with os.path.join / backslashes still
match on Windows.
- Strengthen test_upgrade_preserves_existing_vscode_settings to add a
user-defined key and assert it survives the upgrade (via --force, exercising
the merge + stale-cleanup path) instead of the brittle after == before check.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(workflows): add from_json expression filter
Step outputs captured as strings could never become typed values in
templates - the filter set was default/join/map/contains only, so e.g.
a fan-out items: could never consume a step's JSON stdout. Add an
arg-less from_json pipe filter with parse-or-raise semantics: invalid
JSON or non-string input raises a clear ValueError rather than passing
through silently.
Fixes#2960
* fix(expressions): make from_json strict — reject any arguments
Address review (#2961): from_json('x') and from_json() previously fell through to a silent passthrough of the unparsed value. Reject any parenthesized form with a clear error so mis-wired templates fail loudly. Rename test to ...parses_object (JSON under test is an object) and add coverage for the strict no-arguments behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(workflows): document the from_json expression filter
Address Copilot review: the user-facing filter references omitted the
newly added `from_json` filter. Add it to the ARCHITECTURE.md filter table
(with the `{{ steps.emit.output.stdout | from_json }}` example) and to the
filter enumerations in workflows/README.md and docs/reference/workflows.md
so the docs match the evaluator's capabilities.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): make from_json strictness reject trailing tokens; fix docstring
Address Copilot review:
- Strictness only rejected parenthesized forms, so typos like
`| from_json)` or `| from_json extra` still fell through to the
unknown-filter path and silently returned the unparsed value. Match on
the leading filter token and require the whole filter to be exactly
`from_json`, so every mis-wired form raises. Extend the rejection test to
cover the trailing-token cases.
- The module docstring claimed "no imports", which is misleading now that
the module imports `json`. Reword to state the actual sandbox guarantee:
templates cannot do file I/O, import modules, or run arbitrary code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Initial plan
* Add init workflow step to bootstrap projects like `specify init`
* Address review: simplify stderr capture and extract VALID_SCRIPT_TYPES
* Address review: fail fast on non-empty dir, stdout fallback, README force fix
* Populate exit_code/stdout/stderr in non-empty-dir fast-fail
* fix: address three unresolved review comments in InitStep
- Use `with os.scandir(...)` context manager so the iterator is always
closed even when `any()` short-circuits, preventing file-descriptor
leaks in long-running workflow runs.
- Guard `os.chdir(prev_cwd)` in the `finally` block with a try/except
so an `OSError` (e.g. directory deleted) doesn't bypass returning
the captured `StepResult`.
- Reject non-string `script` values in `validate()` with a clear error
message, rather than silently passing them through to become
`--script True` at runtime.
* Potential fix for pull request finding 'Empty except'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* fix: remove no_git and branch_numbering options removed upstream
The --no-git and --branch-numbering flags were removed from `specify init`
on main. Update InitStep to drop these unsupported config fields and fix
tests accordingly.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address review — integration defaults, integration_options, engine-owned dirs
- Apply DEFAULT_INIT_INTEGRATION fallback when neither step config nor
workflow context provides an integration, so output.integration always
reflects the actual integration used.
- Add integration_options config field to support --integration-options
passthrough (required for generic integration and --skills mode).
- Exclude .specify/ from the non-empty directory fast-fail check so that
here: true works when the engine has already created its run-state
directory before steps execute.
- Note: mix_stderr=False is not needed — Click 8.2+ captures stderr
separately by default and the existing try/except handles access.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: implicitly add --force when only engine-owned dirs exist
When the workflow engine creates .specify/workflows/runs/ before steps
execute, the directory is technically non-empty. Previously, specify init
would prompt for confirmation (hanging in unattended mode) unless the
user explicitly set force: true. Now the step detects that only
engine-owned directories (.specify/) are present and implicitly adds
--force so init proceeds without user interaction.
Also fixes the test to exercise the implicit-force path rather than
passing force: True explicitly (which bypassed the check entirely).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: derive VALID_SCRIPT_TYPES from shared constant, fail fast on OSError, include all resolved fields in output
- Derive VALID_SCRIPT_TYPES from SCRIPT_TYPE_CHOICES in _agent_config
so the valid set cannot drift from the specify init CLI.
- Fail fast with a clear error when os.scandir() raises OSError (e.g.
permission denied) instead of silently treating the directory as empty.
- Include preset, force, and ignore_agent_tools in all output dicts
(both fast-fail and normal paths) for consistent interpolation and
debugging downstream.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: populate stderr from stdout on older Click, fix force comment wording
- When Click does not expose result.stderr (older versions where stderr
is mixed into stdout), use stdout as stderr on non-zero exit so
workflows can consistently read steps.<id>.output.stderr for errors.
- Update README inline comment for force: wording to say 'when target
directory already exists' rather than 'non-empty directory', matching
the actual specify init behavior for the project: form.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: build argv flags before early returns, use any() for dir scan
- Move argv flag-building (--integration, --script, --preset,
--ignore-agent-tools) before the non-empty-dir and OSError early
returns so output['argv'] always reflects the complete command.
- --force is appended after the check since it may be set implicitly.
- Replace list comprehension with any() generator expression to
short-circuit without allocating a full list of DirEntry objects.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: only treat .specify as engine-owned when it is a real directory
A file or symlink named .specify should not be excluded from the
non-empty check. Use entry.is_dir(follow_symlinks=False) to ensure
only an actual directory is considered engine-owned content.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: guard implicit force for engine dirs only, fix integration fallback order
- Only set implicit --force when engine-owned directories (.specify/)
are actually present. A completely empty directory no longer gets
--force added unnecessarily.
- Fix integration resolution precedence: resolve step config expression
first, then fall back to workflow default (also resolved), then to
DEFAULT_INIT_INTEGRATION. Previously, a step expression resolving to
falsy would bypass the workflow default entirely.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: bump version to 0.11.1
* chore: begin 0.11.2.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* chore: ignore Copilot dogfooding scaffolding in .gitignore
Ignore the directories and files generated by
`specify init --integration copilot` so the dogfooding scaffolding used
during Spec Kit feature development isn't accidentally committed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: fix gitignore trailing whitespace in comment
Remove trailing whitespace and extra comment-only lines in the Copilot dogfooding ignore block.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(workflows): opt-in output_format: json exposes parsed shell stdout as output.data
No step that runs external code could hand a typed value to a later
step, so e.g. a fan-out could never consume a runtime-computed
collection. With output_format: json declared, stdout is parsed and
exposed under output.data (raw keys unchanged); a parse failure fails
the step with a clear error. Without the key, behavior is unchanged.
Reference implementation for the proposal in #2962.
Addresses #2962
* test(shell): emit JSON via sys.executable for cross-platform output_format tests
Address review (#2963): replace non-portable echo '{...}' (Windows cmd.exe keeps the single quotes, breaking JSON) with the established '"{py}" "{script}"' pattern using sys.executable + a temp script, so the output_format tests pass on the Windows CI matrix. Also make the validate test's run inert (exit 0).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix: non-zero exit code when a workflow run ends failed or aborted
workflow run and workflow resume printed Status: failed (or emitted the
--json payload) and exited 0, so scripts and CI could not rely on the
process exit code. Map terminal outcomes: failed|aborted -> 1,
completed|paused -> 0, on both the text and --json paths.
The previous exit-0-on-failed behavior was pinned by
test_workflow_run_failing_yaml_without_project; the pin is updated to
the new contract.
Fixes#2958
* test: portable exit-code step commands + cover resume failed->exit-1
Address review (#2959): replace non-portable run: 'true'/'false' with 'exit 0'/'exit 1' (Windows cmd.exe has no true/false builtins under shell=True), and add an end-to-end 'workflow resume' test asserting a resumed failed run exits non-zero.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(skills): preserve non-ASCII chars in skill frontmatter
Skill SKILL.md frontmatter descriptions containing non-ASCII
characters were escaped to \uXXXX / \xXX sequences because
yaml.safe_dump() was called without allow_unicode=True.
- Add allow_unicode=True to the 7 skill/command frontmatter
safe_dump sites (extensions, presets, claude integration)
- Add regression tests for the render and extension-install paths
Follows the approach of #1936; encoding="utf-8" is already set on
the affected write paths, so no encoding change is needed here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(_utils): add dump_frontmatter helper
Centralize skill/command frontmatter YAML serialization into a single
_utils.dump_frontmatter helper so no call site can drop allow_unicode or
diverge on formatting. Route the 7 existing sites through it and drop a
now-unused local yaml import.
Switch the extension test fixtures to yaml.safe_dump for parity with the
production safe-dump/safe-load codepaths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: prevent extension self-install from deleting source dir (#2990)
`specify extension add <path> --dev --force` permanently deleted the
extension directory without registering it when the source path resolved
to the extension's own install location (`.specify/extensions/<id>`).
With `--force`, `install_from_directory()` removed the existing
installation (the source) and then `shutil.copytree()` tried to copy from
the now-deleted directory, destroying it and crashing.
Add a guard that fails fast with a clear ValidationError when the resolved
source path equals the install destination, before any destructive
operation runs. Includes a regression test asserting the directory and its
contents survive.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: harden extension self-install guard
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: disable Rich Live transient mode on Windows to prevent PS 5.1 hang
PowerShell 5.1's legacy console host does not reliably support VT escape
sequences. Rich's Live(transient=True) attempts cursor restoration on
context exit, which hangs indefinitely on that console.
Set transient=False when sys.platform == 'win32' in both init.py (progress
tracker) and _console.py (select_with_arrows). The only cosmetic effect is
that progress output remains visible after completion on Windows.
Fixes#2927
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: address review feedback on test quality
- Use captured['transient'] instead of .get() for clearer KeyError on failure
- Source guards now assert both the platform check AND transient=_transient usage
- Remove unused imports (MagicMock retained as it's used, removed pytest)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: use regex in source guards for resilience to formatting changes
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: use single DOTALL regex to verify assignment flows into Live()
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: skip duplicate tracker print on Windows when transient=False
When transient is False, Rich leaves the Live output on screen. The
subsequent console.print(tracker.render()) would duplicate it. Gate
it behind _transient so Windows users see the tracker exactly once.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: bump version to 0.11.0
* chore: begin 0.11.1.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Initial plan
* Add workflow step catalog: StepRegistry, StepCatalog, CLI commands, and tests
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/2885e646-477d-4df8-b9a3-06d8cb29e748
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
* Potential fix for pull request finding 'An assert statement has a side-effect'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* Address PR review: path traversal, cache robustness, collision check, failed-to-load display
- Add resolve()+relative_to() path traversal guards in workflow_step_add and
workflow_step_remove to prevent directory escape via step_id
- Harden _is_url_cache_valid in both StepCatalog and WorkflowCatalog to
coerce fetched_at to float and catch TypeError/ValueError
- Check STEP_REGISTRY and StepRegistry before installing to prevent
collisions with built-in step types or already-installed steps
- Show 'Custom (installed, failed to load)' section in workflow step list
for steps in the registry that failed to load into STEP_REGISTRY
* Fix StepRegistry shape validation and StepCatalog empty-YAML handling
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/0dca6393-f5a9-40de-bb5c-77ba6af033d2
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
* Polish: rename _default to default_registry, strengthen unreadable-file test
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/0dca6393-f5a9-40de-bb5c-77ba6af033d2
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
* Address PR review: atomic install, hostname validation, cache resilience, no dynamic imports in list/info
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/3e18fef0-e2e6-4b3e-9e8d-9adb1e5e464e
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
* Fix shutil.move with existing step_dir: remove before move to avoid subdirectory nesting
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/3e18fef0-e2e6-4b3e-9e8d-9adb1e5e464e
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
* Call load_custom_steps at execution time; enforce hostname in _safe_fetch and _validate_url
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/73865880-fb25-4061-a43e-4e4b4d1c4de6
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
* Wrap YAML parsing in try/except; atomic step install via os.rename() under same fs
Agent-Logs-Url: https://github.com/github/spec-kit/sessions/ff915bc5-ec7e-4e6a-b505-35f5795250df
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
* Validate YAML root is a dict in _load_catalog_config and workflow_step_add; fix WorkflowCatalog hostname validation
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
* Fix load_custom_steps() package imports and add reserved step ID validation
* Move _re/_sys imports out of loop and _RESERVED_STEP_IDS to module level
* Address review: collision-resistant module names, extra_files support, remove orphan dir
* Harden extra_files: warn on non-dict, resolve symlinks in path traversal check
* Switch _safe_fetch and StepCatalog._fetch_single_catalog to use open_url for auth consistency
* Harden step_id validation against path-segment tricks; raise on StepRegistry.save() OSError
* Clean up sys.modules on broken step packages; handle StepValidationError in step add/remove
* Address review thread: int-coerce priorities, sys.modules cleanup, _require_specify_project, registry-first remove
* fix: normalize workflow step catalog metadata fallbacks
* fix: address latest workflow step and catalog review findings
* Handle non-string extra_files keys in workflow step add
* Harden StepRegistry symlink reads and extra_files path/URL validation
* Harden custom step loader and step remove against symlinks and OSError
* Fix StepCatalog.search() to coerce non-string fields before joining
* Fix WorkflowCatalog YAML parsing error handling and isinstance checks
* Harden step registry save and custom step/catalog ID handling
* Harden cache validation and staging OSError handling
* Address review: reorder symlink guard and split mixed test
- Move symlink-parent check before is_dir() in load_custom_steps() so
we never stat an external target through a symlink
- Split test_get_merged_steps_normalizes_list_ids_to_strings into two
focused tests: one for list-id normalization, one for get_step_info
return values
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address review: symlink-before-stat in loader, restore registry on rmtree failure
- load_custom_steps(): check is_symlink() before is_dir() on step
directories so symlinked entries are skipped without statting external
targets
- workflow_step_remove: restore the registry entry when shutil.rmtree()
fails so filesystem and registry state stay consistent and a future
'step add' isn't blocked
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Harden step_id validation and file-write error handling
- _validate_step_id_or_exit: reject whitespace-only/padded IDs,
Windows-invalid characters (<>:"|?*), control characters, trailing
dots/spaces, and Windows reserved device names (con, nul, etc.)
- Wrap step.yml/__init__.py staging writes in OSError handler
- Wrap extra_files disk writes (mkdir + write_bytes) in OSError handler
that names the failing relative path
- Registry rollback on rmtree failure: restore verbatim metadata and
emit a warning if the restore itself fails
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Address review: cache symlink guard, verbatim registry rollback, Windows test fix
- StepCatalog: add _is_cache_path_safe() guard that checks for symlinks
in .specify/workflows/steps/.cache path; skip cache reads and writes
when any component is symlinked to prevent writes outside project root
- Registry rollback: write metadata directly to registry.data['steps']
and call save() instead of using add() which overwrites timestamps
- temp_dir fixture: use ignore_errors=True on Windows to avoid flaky
teardown from locked file handles (WinError 32)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Simplify exec_module call by removing redundant nested try/except
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix empty YAML tolerance in WorkflowCatalog.add_catalog, scope ignore_errors to Windows
- WorkflowCatalog.add_catalog(): treat None from yaml.safe_load() (empty
file) as an empty mapping instead of raising 'corrupted'
- temp_dir fixture: limit ignore_errors to sys.platform == 'win32' so
real cleanup issues surface on non-Windows platforms
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Chain exceptions in _load_catalog_config for both catalog classes
Add 'from exc' to preserve root cause in tracebacks while keeping
clean user-facing messages.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Make default catalog tests hermetic by isolating HOME
Monkeypatch Path.home() to project_dir and clear catalog env vars so
tests don't break on machines with a real ~/.specify/step-catalogs.yml
or ~/.specify/workflow-catalogs.yml.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fix falsy ID handling in _get_merged_steps for list-based catalogs
Check for None explicitly instead of using 'or' which drops valid
falsy IDs like 0.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Compare reserved step IDs case-insensitively for filesystem safety
On case-insensitive filesystems (Windows, common macOS), variants like
STEP-REGISTRY.JSON would collide with the actual registry file.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Add explanatory comments to intentional empty except blocks
Document why cache-read failures are silently ignored in both
WorkflowCatalog and StepCatalog _fetch_single_catalog methods.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(dev): add integration scaffolder
* fix(dev): address integration scaffold review feedback
* fix(dev): address scaffold follow-up review
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix(dev): default scaffolded integrations to multi_install_safe = False
The scaffold template emitted `multi_install_safe = True` alongside a
placeholder `context_file = "AGENTS.md"`. Registered as-is, that violates the
registry contract (test_safe_integrations_have_distinct_context_files): codex
already pairs AGENTS.md with multi_install_safe = True, so the generated
boilerplate would collide on first registration.
Default the scaffold to False (matching IntegrationBase) so generated code is
registry-test-friendly out of the box; contributors opt in once they pick a
unique context_file. Aligns the generated test skeleton and both scaffold
tests, which previously contradicted each other (one expected True, one False).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(dev): harden scaffold writes and accept case-insensitive --type
- Guard scaffold_integration() against symlinked target directories: walk
each path component under the repo root and refuse symlinked dirs, then
confirm the write destination resolves inside the repo (mirrors the
manifest directory guard). Prevents scaffolding outside the repo when a
contributor's integrations/tests path is symlinked.
- Make the `--type` click.Choice case-insensitive so `--type YAML` is
accepted, matching scaffold_integration()'s strip()/lower() normalization
instead of rejecting at the CLI layer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(dev): report scaffold filesystem failures as a clean CLI error
The `dev integration scaffold` command only caught FileExistsError/ValueError,
so an OSError raised during mkdir()/write_text() (permission denied, read-only
checkout, a path component that is a file, ...) bubbled up as a traceback
instead of a clean error + exit code. Broaden the handler to OSError (which
also covers FileExistsError) and add coverage for the filesystem-error path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(dev): move scaffold command under integration
* fix(dev): roll back partial scaffold writes
* fix(dev): correct lint docs and generated test docstring
- local-development.md: ruff check src/ is enforced in CI, not absent
- scaffolded test docstring: drop misleading 'scaffold' wording
* fix(scaffold): create only leaf integration directory
---------
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>
* feat: add Zed integration
* fix: update integrations stats grid to 31 for consistency
* fix: address Copilot review feedback
- Remove non-actionable --skills flag from ZedIntegration (Zed is always
skills-based, like Agy)
- Align zed_skill_mode predicate with ai_skills for consistency across
init output and hook rendering
- Consolidate claude/cursor/zed slash-skill return blocks in
_render_hook_invocation to reduce duplication
- Override test_options_include_skills_flag for Zed (no --skills flag)
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix: address Copilot review round 2
- Make zed_skill_mode unconditional in hook rendering (Zed is always
skills-based, no --skills option)
- Add test_init_persists_ai_skills_for_zed that exercises the actual
CLI init path and verifies HookExecutor renders /speckit-plan
without manual init-options manipulation
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix: address copilot review feedback for zed integration
- Update integration count from 31 to 33 in docs/index.md (32 integrations + Generic)
- Make zed_skill_mode unconditional to match extensions.py behavior
- Consolidate slash-skill integrations into a set for consistency
- Move os import to module level in test_integration_zed.py
* fix: refine slash-skill logic and ai-skills validation
- Fix slash-skill integrations: Claude/Cursor require ai_skills=true; Zed/Agy/Devin are always skills
- Allow --ai-skills with --integration (not just --ai) to fix validation error
* fix: remove unused variables and update ai-skills help text
- Add agy_skill_mode and devin_skill_mode variables to fix F841 lint error
- Use all skill mode variables in the slash-skill conditional check
- Update --ai-skills help text to reflect it works with --integration too
* fix: add trae_skill_mode to hook invocation for consistency
Trae is a SkillsIntegration like Zed/Agy/Devin, so it should also be treated
as always-skills-based in hook invocation rendering.
* fix: make Agy always skills-based for consistency
AgyIntegration is a SkillsIntegration subclass with no --skills option,
so it should be treated as always skills-based (like Zed, Devin, Trae).
This aligns init.py skill mode detection with extensions.py hook rendering.
* fix: gate agy_skill_mode and refactor _render_hook_invocation to use sets
Addressed Copilot review comments:
- Restored _is_skills_integration guard on agy_skill_mode in init.py
to be defensive about runtime integration type.
- Refactored _render_hook_invocation() in extensions.py to use
always_slash/conditional_slash frozensets instead of individual
per-agent booleans, eliminating unused variables (F841) and making
it harder for conditions to drift between integrations.
- Centralized slash-skill determination so adding a new unconditional
slash-skill integration is a one-key addition.
* fix: address latest Copilot review comments
- Added copilot to CONDITIONAL_SLASH_AGENTS for consistent
hook invocation rendering with init.py
- Moved always_slash/conditional_slash frozensets to module
scope to avoid per-call reallocation
- Replaced manual os.chdir() with monkeypatch.chdir() in test
- Overrode test_options_include_skills_flag for Zed (no --skills)
* fix: address latest Copilot review comments
- Removed redundant local import yaml in _register_extension_skills
(yaml is already imported at module scope)
- Split --ai-skills usage hint into two separate print statements
for better readability
- Changed integrations count from '33' to '30+' to avoid future drift
* fix: re-add _is_skills_integration definition lost in merge
The _is_skills_integration variable was accidentally dropped during the
web UI merge resolution of upstream/main's removal of legacy --ai flags.
Re-added the definition via isinstance(resolved_integration, SkillsIntegration)
check so that skill-mode booleans work correctly.
* fix: gate zed_skill_mode on _is_skills_integration for consistency
Aligns zed_skill_mode with the other skills-based agents (codex, claude,
cursor-agent, copilot) which all use _is_skills_integration gating.
Since ZedIntegration extends SkillsIntegration, behavior is unchanged.
* fix: remove unused claude_skill_mode and cursor_skill_mode locals in _render_hook_invocation
These variables became unused after the refactor to ALWAYS_SLASH_AGENTS /
CONDITIONAL_SLASH_AGENTS sets. Claude and Cursor-Agent are now handled by the
CONDITIONAL_SLASH_AGENTS path, so the separate boolean locals are dead code.
Fixes ruff F841 and addresses Copilot review feedback that was repeated across
multiple review rounds.
* fix: align agy/trae invocation format in init next-steps with hook rendering and build_command_invocation
- Moved agy and trae from '-<name>' (dollar/Codex format) to
'/speckit-<name>' (slash format) in _display_cmd() to match:
- HookExecutor._render_hook_invocation() (ALWAYS_SLASH_AGENTS for trae,
CONDITIONAL_SLASH_AGENTS for agy)
- SkillsIntegration.build_command_invocation() (default: /speckit-<name>)
- The '$' prefix is specific to Codex; all other skills agents use '/'.
* fix: address Copilot review comments on hook invocation consistency
- Add is_slash_skills_agent() helper to extensions.py to centralize the
agent-to-invocation-format mapping, reducing drift risk between
HookExecutor._render_hook_invocation() and init.py _display_cmd()
- Use the shared helper in both locations; init.py now imports and
delegates to is_slash_skills_agent() instead of maintaining its own
per-agent boolean matrix
- Fix test_hooks_render_skill_invocation to use ai_skills=False,
proving Zed renders /speckit-<name> unconditionally
- Add parameterized TestSlashSkillsSets covering all agents in
ALWAYS_SLASH_AGENTS and CONDITIONAL_SLASH_AGENTS with ai_skills
both true and false
* fix: address Copilot review comments on type safety and test API
- Make is_slash_skills_agent() accept str | None to match its call sites
(init_options.get("ai") can return None)
- Refactor TestSlashSkillsSets to use public execute_hook() API instead of
private _render_hook_invocation() method
* fix: address Copilot review comments on typing and naming clarity
- Add from __future__ import annotations to extensions.py so PEP 604
unions (str | None) are safe regardless of Python version
- Add clarifying _ai_skills_enabled local variable in init.py's
_display_cmd() to make the semantic meaning explicit when passing it
to is_slash_skills_agent()
* fix: move invocation-style logic into shared _invocation_style module
- Extract ALWAYS_SLASH_AGENTS, CONDITIONAL_SLASH_AGENTS, and
is_slash_skills_agent() from extensions.py into new _invocation_style.py
module, eliminating the awkward init.py -> extensions.py import
dependency for invocation-style decision logic
- Both HookExecutor._render_hook_invocation() and init.py _display_cmd()
now import from the shared module instead of one subsystem importing
from the other
- Revert /SKILL.md change: the leading slash is semantically significant
(path component vs filename suffix)
* fix: add None guard before i.options() in test_options_include_skills_flag
get_integration() returns IntegrationBase | None, so i.options()
is a type error without a None check.
* fix: override test_options_include_skills_flag for Zed (always skills, no --skills flag)
Zed is always skills-based and doesn't expose a --skills option.
Override the inherited base test to assert --skills is absent.
* fix: rename test and skip inherited test_options_include_skills_flag for Zed
- Skip inherited test_options_include_skills_flag (not applicable — Zed
is always skills-based with no --skills flag)
- Add test_options_do_not_include_skills_flag with correct name matching
the assertion (--skills is absent)
* fix: add defensive non-string check in is_slash_skills_agent
Reject non-string values for selected_ai to prevent TypeError from
set membership checks when persisted init-options contain corrupted
data (e.g. list or dict instead of string).
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
CITATION.cff was created at v0.7.3 (2026-04-17) and has not been
updated since. The latest stable release is v0.10.2, released on
2026-06-11. This brings the citation metadata in sync with the
published release so tools that ingest CITATION.cff (Zenodo, GitHub's
"Cite this repository" widget, citation managers) surface the correct
version.
Verification:
- `gh release list --repo github/spec-kit --limit 1` → v0.10.2 / 2026-06-11
- CHANGELOG.md `## [0.10.2] - 2026-06-11` confirms the date
- pyproject.toml `version = "0.10.3.dev0"` confirms 0.10.2 is latest stable
AI-assisted contribution.
* chore: bump version to 0.10.4
* chore: begin 0.10.5.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
A non-list result from the items expression is a wiring error (the
template did not resolve to a collection); silently fanning out over
zero items hides it until a confusing downstream failure. Fail the
step with an error naming the expression instead. An explicit empty
list remains valid input.
Fixes#2956
* refactor(presets): convert presets.py module to presets/ package
Pure structural move to mirror integrations/. presets.py becomes
presets/__init__.py with relative imports rebased one level deeper.
No behavior change; public import surface (from .presets import ...)
preserved. Prepares for co-locating preset command handlers in PR-6/8.
* refactor: move preset command handlers to presets/_commands.py (PR-6/8)
Cut the preset_app / preset_catalog_app Typer groups and all 12 command
handlers out of __init__.py into presets/_commands.py, exposing register(app)
— mirrors the integration co-location from PR-5. __init__.py now registers
via _register_preset_cmds(app), dropping ~620 lines (3282 -> 2663).
Handlers lazy-import root helpers (_require_specify_project, get_speckit_version,
_locate_bundled_preset, _display_project_path) via 'from .. import' so test
monkeypatching of specify_cli.<helper> keeps working. _locate_bundled_preset
kept as an explicit re-export in __init__.py for that resolution path.
CLI surface and public imports unchanged. Full suite: 3162 passed, 40 skipped.
* docs: add guide for handling complex features
Add a Concepts page documenting strategies for dealing with large or
complex features where context window exhaustion degrades agent
performance during implementation. Covers limiting tasks per run,
sub-agent delegation, combining both, and decomposing into smaller
specs, with a guideline table for choosing an approach.
Closes#2986
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: address review feedback on complex features guide
Use task IDs (T001-T010) instead of bare numbers to match the tasks.md
template format, and add the combined scoping + delegation approach to
the selection table for completeness.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: align complex features guide with command naming conventions
Use the full /speckit.implement command name throughout, match the
command template wording ('must consider'), and use the product names
GitHub Copilot CLI and the GitHub Copilot extension for VS Code.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: bump version to 0.10.3
* chore: begin 0.10.4.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* chore: bump version to 0.10.2
* chore: begin 0.10.3.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Add Spec Trace extension to community catalog
* docs(catalog): mark Spec Trace as Read+Write
The /speckit.trace.build command writes .specify/trace.md, so the
catalog row's Effect column was wrong. Aligning with the extension's
documented behavior.
* docs(community): add Spec Trace row to extensions.md
The public community extensions table moved from README.md to
docs/community/extensions.md per the repo convention documented in
.github/skills/add-community-extension/SKILL.md. Adding the Spec Trace
row alphabetically between Spec Sync and Spec Validate so the doc stays
in sync with the catalog entry already added.
* fix(catalog): use literal Unicode characters in Spec Trace description
Copilot's review on this PR noted that the Spec Trace entry was the
only one in catalog.community.json using JSON Unicode escape sequences
(\u2192 for the arrow, \u2014 for the em-dash). Every other entry
that uses those characters writes them as literal multi-byte UTF-8
(18 entries with literal em-dash, 5 with literal arrow), so the
escaped form made this row harder to read and review in plain text
and stood out as the only inconsistency in the file.
Replacing the escapes with the literal characters keeps the entry
visually consistent with the rest of the catalog and decodes to the
same string at runtime, so no consumer changes.
* chore(catalog): set Spec Trace timestamps to catalog-add date
Per add-community-extension SKILL.md, a new entry's created_at/updated_at
should reflect the date it is added to the catalog, and the top-level
catalog updated_at must be refreshed on any add. Set the Spec Trace
entry and the catalog-level updated_at to 2026-06-09.
* docs(community): categorize Spec Trace as code
Spec Trace analyzes the test suite (source) and produces a coverage/
traceability report, matching the documented 'code' category (reviews/
validates source) rather than 'process' (orchestrates workflow across
phases). Aligns with the sibling SpecTest row.
Extension-provided commands that declare `argument-hint:` in their
frontmatter had that field dropped from the generated Claude
`.claude/skills/<name>/SKILL.md`, while core template commands keep it.
The extension skill generator built the frontmatter via the shared
build_skill_frontmatter() (name/description/compatibility/metadata only)
and never forwarded argument-hint.
Carry argument-hint from the parsed source command frontmatter into the
skill frontmatter dict before serialization, gated on the integration
exposing inject_argument_hint so only argument-hint-aware agents (Claude)
receive the key and build_skill_frontmatter's shared shape stays unchanged
for every other agent. The value is injected into the dict rather than via
the string-based inject_argument_hint helper, so a folded multi-line
description cannot be split into invalid YAML.
Add regression tests covering a folding description (Claude) and the
non-Claude gate (kimi).
Closes#2903
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Harden preset URL installs against unsafe redirects
Preset URL installs already rejected non-HTTPS source URLs, but the authenticated opener follows redirects. Validate the final response URL before writing the ZIP, preserve GitHub release asset URL resolution after the preset command module split, stream the response to disk, and keep catalog config serialization on safe YAML output.
Constraint: open_url follows redirects, so source URL validation alone does not constrain the downloaded target
Rejected: Keep response.read() for simplicity | large preset downloads should not be buffered entirely in memory
Confidence: high
Scope-risk: narrow
Directive: Keep preset URL policy aligned with workflow installer redirect validation
Tested: uvx ruff check src/specify_cli/__init__.py src/specify_cli/presets/__init__.py src/specify_cli/presets/_commands.py tests/test_presets.py
Tested: uv run pytest tests/test_presets.py -q
Not-tested: Real network redirect integration against a live HTTP server
Co-authored-by: OmX <omx@oh-my-codex.dev>
* Reject malformed preset download URLs
Preset downloads should fail early when a URL lacks a hostname, even if the scheme is HTTPS. The redirect error now describes any disallowed target instead of implying that only non-HTTPS redirects are blocked.
* Prevent credentialed preset redirects from downgrading transport
Preset URL downloads already checked the final URL after urllib followed redirects, but that was too late for authenticated requests because same-host redirects could preserve Authorization during the redirect itself. The authenticated HTTP helper now supports an opt-in redirect validator, and preset downloads use it to reject disallowed redirect targets before following them. The redirect auth handlers also stop preserving credentials across HTTPS to non-HTTPS downgrades as defense in depth.
* test(presets): 修复 URL 解析测试 mock 缺少 redirect_validator 参数
重定向安全加固为 open_url 新增 redirect_validator 参数,
两处 fake_open_url mock 签名未同步导致 TypeError。
补齐参数后全部 3717 个测试通过。
---------
Co-authored-by: OmX <omx@oh-my-codex.dev>
_is_managed() in install_shared_infra now consults manifest.is_recovered()
before treating a hash-matching file as managed. Files marked recovered
(pre-existing on disk, not installed by Spec Kit) are no longer overwritten
by integration use/switch even when their hash matches the manifest entry.
This closes the gap documented in the manifest API: callers using
refresh_managed MUST check is_recovered first.
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: add category and effect as first-class fields in extension schema
Add `category` and `effect` as optional fields in the extension schema
(`extension.yml`) and community catalog (`catalog.community.json`).
Schema changes:
- Valid categories: docs, code, process, integration, visibility
- Valid effects: read-only, read-write
- Both fields are optional (backward-compatible with existing extensions)
- Validation raises ValidationError for invalid values when present
Propagation:
- Added `category` and `effect` to all 108 entries in catalog.community.json
(populated from the existing docs/community/extensions.md table)
- Updated extension template with commented category/effect fields
- Updated add-community-extension skill with new JSON template fields
- Updated `specify extension info` CLI output to display category/effect
- Added properties to ExtensionManifest class
Tests:
- test_valid_category: all 5 category values pass
- test_valid_effect: both effect values pass
- test_invalid_category: invalid value raises ValidationError
- test_invalid_effect: invalid value raises ValidationError
- test_category_and_effect_optional: omitting fields still works
Closes#2874
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: make category free-form, keep effect validated
Category is a free-form string (only validated as non-empty when present),
while effect remains restricted to 'read-only' or 'read-write'.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address PR review feedback
- Add type guard before 'in' check for effect to prevent TypeError on
unhashable YAML values (list/dict)
- Comment out category/effect in template so authors must opt in
- Use VALID_EFFECTS constant in test instead of hard-coded values
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: update category docstring to reflect free-form semantics
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: clarify canonical extension effect values
---------
Co-authored-by: Manfred Riem <mnriem@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(catalog): add Jira Integration (Sync Engine) extension
Adds a new community-catalog listing for `spec-kit-jira-sync`
(ashbrener/spec-kit-jira-sync), a reconcile-engine bridge that mirrors
spec-kit specs into Jira (Epic per repo, Story per spec, Subtask per
phase): idempotent, drift-aware, fail-closed.
Catalog id is `jira-sync` because the `jira` id is already taken by an
unrelated extension; display name "Jira Integration (Sync Engine)"
disambiguates from the existing "Jira Integration" listing.
Touches the two catalog surfaces:
1. extensions/catalog.community.json - the new "jira-sync" entry,
inserted after the existing "jira" entry. Field shape matches the
sibling "linear" entry exactly.
2. docs/community/extensions.md - the table row, after the existing
Jira Integration row.
JSON validated; diff is the single entry + the one table row.
* catalog(jira-sync): neutral capability-focused description (address Copilot review)
Drop the comparative/absolute framing ('A real …', 'never corrupts your board')
flagged by Copilot; keep the factual, tested capability descriptors (idempotent,
drift-aware, fail-closed). Applies to both the catalog entry and the docs table row.
* chore(catalog): bump jira-sync to v0.2.0 (re-mode + engine unification)
* fix(catalog): jira-sync download_url .tar.gz -> .zip (installer is ZIP-only)
The spec-kit extension installer saves {id}-{version}.zip and extracts via
zipfile.ZipFile (src/specify_cli/extensions.py) — a .tar.gz asset downloads but
fails extraction. Matches every other catalog entry's /archive/refs/tags/vX.zip
convention. Addresses the Copilot review on PR #2895.
---------
Co-authored-by: Ash Brener <ashley@midletearth.com>
* chore: bump version to 0.10.1
* chore: begin 0.10.2.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* chore(catalog): bump linear to v0.3.0 + spec-kit-linear-sync URLs
The Linear extension repo was renamed ashbrener/spec-kit-linear -> spec-kit-linear-sync
and shipped v0.3.0. Update the community catalog entry's download_url (was pinned to
v0.2.0), repository/homepage/documentation/changelog URLs, and version. extension id
stays 'linear' (commands unchanged); old GitHub URLs redirect.
* docs(community): point Linear extension table row at spec-kit-linear-sync
---------
Co-authored-by: Ash Brener <ashley@midletearth.com>
Bump the docguard community catalog entry 0.9.11 -> 0.25.0, point the
download at the v0.25.0 release asset, and update the description to
reflect the single pinned runtime dependency (@babel/parser, added in
v0.24 for AST-based validation). Sync the docs/community table row to
match. Rebased onto current main to clear the prior merge conflict.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
open_github_url() was orphaned when #2393 moved download authentication
to the config-driven registry in authentication/http.py; its dedicated
_StripAuthOnRedirect handler was referenced only by open_github_url
itself and duplicated the live implementation in authentication/http.py.
Remove both, keep the live resolve_github_release_asset_api_url() and
the tested build_github_request()/GITHUB_HOSTS utilities, and update
the module docstring to match what the module does today.
No runtime behavior change.
Closes#2876
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(catalogs): validate extension and preset catalog payload shape
`ExtensionCatalog._fetch_single_catalog` and
`PresetCatalog._fetch_single_catalog` only check that the `extensions` /
`presets` key is *present* in the parsed catalog JSON. They don't check
that the value is a JSON object, and they don't check that the root is
a JSON object at all. A malformed (or compromised) upstream catalog
returning:
{"schema_version": "1.0", "extensions": []}
passes both `"extensions" not in catalog_data` and the subsequent
`response.read()` JSON parse, gets cached on disk, and then crashes
deep inside `_get_merged_extensions` (resp. `_get_merged_packs`) with:
AttributeError: 'list' object has no attribute 'items'
instead of the existing user-facing
`ExtensionError("Invalid catalog format from <url>")` /
`PresetError("Invalid preset catalog format")` that the surrounding
code is clearly trying to produce.
The sibling integration-catalog reader already validates this — see
`src/specify_cli/integrations/catalog.py` where the fetch path
explicitly checks both `isinstance(catalog_data, dict)` and
`isinstance(catalog_data.get("integrations"), dict)` before returning.
This change mirrors that pattern in the extension and preset readers so
the three catalog fetchers stay consistent and a malformed upstream
surfaces as the user-facing error instead of a raw Python traceback.
Adds parametrized regression tests covering:
- root payload is not a JSON object (list, str, int, null)
- root is a dict but `extensions` / `presets` value is the wrong type
(list, str, null, int)
All eight bad-payload shapes now raise the expected catalog error.
* fix(catalogs): skip non-mapping entries during extension and preset merge
Addresses Copilot review feedback on this PR.
`_fetch_single_catalog` now validates that the ``extensions`` / ``presets``
value is a mapping, but it doesn't (and shouldn't) validate every entry
inside that mapping. A payload like:
{"schema_version": "1.0", "extensions": {"good": {...}, "bad": []}}
passes the fetch-level guard, then later crashes inside
``_get_merged_extensions`` (resp. ``_get_merged_packs``) at
``{**ext_data, ...}`` with ``TypeError: 'list' object is not a mapping``.
The sibling integration-catalog reader at
``src/specify_cli/integrations/catalog.py:245`` handles this with a
per-entry ``isinstance(integ_data, dict)`` skip during merge, so one
malformed entry doesn't poison an otherwise valid catalog. This change
mirrors that pattern in the extension and preset mergers and adds
regression tests asserting that valid entries continue to merge while
malformed siblings are silently dropped.
* fix(catalogs): validate cached extension and preset payload shape
Addresses Copilot review feedback on this PR (round 2).
The earlier commits in this branch added payload-shape validation on the
network fetch path. The cache-hit path still returned
``json.loads(cache_file.read_text())`` directly without re-checking the
shape, so a cache poisoned by an older spec-kit version (or a manual
edit, or an upstream that briefly served a bad payload before the
network guards landed) would re-crash every invocation of
``_get_merged_extensions`` / ``_get_merged_packs`` with
``AttributeError: 'list' object has no attribute 'items'`` despite the
cache being "valid" by age.
Extracts the shape validation into ``_validate_catalog_payload`` on both
``ExtensionCatalog`` and ``PresetCatalog``, and calls it from both the
cache-load and network-fetch branches of ``_fetch_single_catalog``. If
the cached payload fails validation, the cache read is treated like a
``json.JSONDecodeError`` — the cached value is discarded and the
function falls through to the network fetch, which refreshes the cache
with a clean payload on success. Never propagates ``AttributeError`` to
the caller.
Regression tests parametrize the four root-bad-type variants plus three
``extensions``/``presets``-bad-type variants per file, asserting that a
poisoned cache silently recovers via network refetch and returns the
freshly-fetched payload.
* fix(catalogs): include URL in missing-keys error to match sibling branches
Addresses Copilot review feedback on this PR (round 3).
``_validate_catalog_payload`` advertises in its docstring that the
catalog URL is included in error messages "so the user can tell which
catalog in a multi-catalog stack is malformed" — but the missing-keys
branch raised ``PresetError("Invalid preset catalog format")`` without
the URL, breaking that contract and making multi-catalog debugging
harder. The root-bad-type and nested-bad-type branches in the same
helper already include the URL; this commit brings the middle branch
in line.
For consistency, the same fix is applied to the legacy single-catalog
fetch paths in ``ExtensionCatalog.fetch_catalog`` and
``PresetCatalog.fetch_catalog`` (where the URL was likewise dropped
from the missing-keys error).
The existing regex matchers in the regression tests target the
``"Invalid (preset )?catalog format"`` prefix, which is preserved
verbatim before the ``from <url>`` suffix — no test changes needed.
* fix(catalogs): broaden cache except tuples and reuse validator in fetch_catalog
Addresses Copilot review feedback on this PR (round 4):
1. ``ExtensionCatalog.fetch_catalog`` and ``PresetCatalog.fetch_catalog``
— the legacy single-catalog methods — still only checked key
presence. A payload like ``42`` (root non-object) crashed with
``TypeError: argument of type 'int' is not iterable`` during the
``"schema_version" in catalog_data`` check, and an entry mapping of
the wrong type crashed downstream. Both now reuse
``_validate_catalog_payload`` so the network-side behaviour of the
legacy methods stays consistent with the multi-catalog
``_fetch_single_catalog`` path. (Copilot #3335623482, #3335623556.)
2. The cache-read ``except`` tuples in ``_fetch_single_catalog`` and
``fetch_catalog`` were too narrow. ``read_text`` can raise
``OSError`` (permissions / disk / handle limit) or ``UnicodeError``
(cache file written by an older client in a different encoding)
in addition to ``json.JSONDecodeError``. Without those in the
tuple, an unreadable cache crashed the caller instead of falling
through to the network refetch the cache contract documents. Both
sites now catch ``(json.JSONDecodeError, OSError, UnicodeError,
<DomainError>)``. (Copilot #3335623588, #3335623608.)
3. While here, pinned ``encoding="utf-8"`` on every cache ``read_text``
call so cache files written by an older Windows client (with a
non-UTF-8 default locale) decode the same way on a newer client.
Regression tests:
- ``test_fetch_catalog_rejects_malformed_payload`` — 7 parametrized
payloads per file covering root-non-object + nested-bad-type
variants asserting ``fetch_catalog`` raises the named domain error.
- ``test_fetch_catalog_recovers_from_unreadable_cache`` — writes
``b"\xff\xfe\x00not-utf-8"`` to the cache file and asserts
``fetch_catalog`` silently falls through to the mocked network and
returns the freshly-fetched payload.
* fix(catalogs): harden cache-validity checks and pin UTF-8 on writes
The cache-best-effort contract added in 7f44b25 was incomplete on two
points raised by Copilot:
1. The cache-validity helpers (is_cache_valid /
_is_url_cache_valid, plus the inline metadata-age check inside
_fetch_single_catalog for per-URL caches) read the metadata file
without specifying an encoding and only caught
json.JSONDecodeError / ValueError / KeyError /
TypeError. A metadata file written by a tool using the system
locale codec, or one whose handle is briefly unavailable, would
raise UnicodeDecodeError / OSError and propagate past the
read-side try/except in fetch_catalog — the very crash the
read-side guard was meant to prevent. The validity checks now read
with encoding="utf-8" and treat OSError / UnicodeError
as cache-invalid, matching the documented contract.
2. The network-fetch path wrote the cache and metadata files with bare
write_text(...), picking up the platform default encoding. The
read path was already pinned to UTF-8 (and the
integrations/catalog.py:193-203 sibling writes UTF-8 too), so
on hosts whose default codec isn't UTF-8 the write/read pair could
disagree and force an unnecessary refetch on every invocation. All
four write_text calls now pass encoding="utf-8" so the
cache survives a round trip on any platform.
Also rewords the misleading # Fetch from network comment in
extensions.fetch_catalog — it sat above the cache-check block,
which read as if the cache step had been skipped.
Tests
-----
Adds two parametrized regression tests per catalog:
* test_fetch_catalog_recovers_from_unreadable_metadata plants
non-UTF-8 bytes in the metadata file, asserts is_cache_valid()
returns False (rather than raising), and confirms
fetch_catalog falls through to the network instead of crashing.
* test_fetch_catalog_writes_cache_as_utf8 round-trips a payload
containing a non-ASCII identifier (café) through the public
fetch path and reads the cache back with
read_text(encoding="utf-8"), catching encoding drift at the
byte level rather than relying on the system codec to happen to be
UTF-8.
Both pairs follow the established sibling-file symmetry — the
extension and preset suites stay in lock-step.
* test(catalogs): assert UTF-8 write encoding by recording write_text kwargs
Copilot's review on this PR caught that test_fetch_catalog_writes_cache_as_utf8
claimed to validate UTF-8 at the byte level but actually only round-tripped a
non-ASCII string through json.dumps/read_text. Because json.dumps defaults to
ensure_ascii=True, 'café' was serialized as the all-ASCII escape 'caf\u00e9'
before reaching write_text — the bytes on disk were identical regardless of the
encoding kwarg, so a locale-encoded write would have round-tripped just fine.
The drift guard the test name advertised was not actually being enforced.
Rewriting these tests to observe the production code's argument directly:
each test now monkey-patches pathlib.Path.write_text with a recorder that
captures the encoding kwarg for every call, runs the production fetch, and
asserts every write into the cache directory passed encoding='utf-8'. That is
the substantive thing the regression guard cares about — non-ASCII payload
tricks were the wrong lever to pull, because json.dumps was masking the
encoding choice before write_text ever ran.
Both tests verified locally against the current production code (492 passed in
the extensions+presets suites) and confirmed to fail against a synthetic
no-encoding write (the recorder records None instead of 'utf-8', the assertion
catches it). Same change applied symmetrically to test_extensions.py and
test_presets.py to keep the sibling files in lockstep with the production
code paths in extensions.py and presets.py.
* fix(catalogs): catch AttributeError on non-mapping cache metadata; drop stale line refs
Copilot's review on the previous push pointed out that the
cache-validity helpers still had a gap: metadata.get("cached_at", "")
assumes metadata is a dict, but json.loads happily parses a
file containing [] / "oops" / 42 / true / null into
a non-mapping. The except tuple covered json.JSONDecodeError,
OSError, UnicodeError, ValueError, KeyError and
TypeError but not AttributeError, so a valid-JSON-but-non-dict
metadata payload would still crash the caller instead of degrading to
"cache invalid" as the docstring promised.
This affected four cache-validity sites — symmetric across the two
catalog modules:
* extensions.py — inline per-URL metadata-age check in
_fetch_single_catalog
* extensions.py — is_cache_valid (legacy default-URL path)
* presets.py — _is_url_cache_valid
* presets.py — is_cache_valid
All four except tuples now include AttributeError with a comment
naming the exact failure (metadata.get(...) on a non-mapping) so
the next reader doesn't have to reconstruct the reasoning.
Separately, Copilot flagged that several comments hard-coded a line
range from a sibling file
(integrations/catalog.py:193-203) — those references will go stale
the moment that file changes. Replaced the hard-coded ranges with
file-only references (integrations/catalog.py) so the pointer
stays accurate as that file evolves. Same change applied to both
modules.
Tests
-----
test_is_cache_valid_handles_non_mapping_metadata is added to both
test_extensions.py and test_presets.py, parametrized over the
five JSON non-mapping root types ([], "oops", 42,
true, null). Each variant plants the metadata file with that
exact content and asserts is_cache_valid() returns False
without raising. The parametrize covers every JSON type the public
spec allows at the root, so a regression that drops AttributeError
from any except tuple is caught against every observable shape rather
than relying on the next reviewer to remember the .get /
non-mapping interaction.
pytest tests/test_extensions.py tests/test_presets.py — 502
passed (was 492 before; the parametrize adds five vectors per file).
* fix(catalogs): make cache writes best-effort to match read-side contract
* feat(integration): add status reporting
* docs(integration): include status in query command docstring
* fix(integration): handle Windows extended-length paths in status containment
On Windows, os.readlink() (and sometimes Path.resolve()) return paths with
the \\?\ extended-length prefix. Comparing such a target against a plain
project root via Path.relative_to() spuriously fails, so an in-project
dangling symlink was classified as `invalid` instead of `missing` — failing
test_status_treats_dangling_symlink_as_missing and the windows-style variant
on the Windows CI runners.
Centralize the containment check in _is_within_project() and strip the
\\?\ / \\?\UNC\ prefix from both sides before relative_to(). Add portable
regression tests for the prefix-stripping helper and the containment contract.
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(integration): restore top-level pytest import after rebase
A three-way merge / rebase onto main silently dropped the module-level
`import pytest` from test_integration_subcommand.py: main reorganized the
import block without it (using only a local `import pytest as _pytest`),
while this branch added top-level fixtures and `pytest.skip`/`pytest.raises`
usage. The overlapping import-hunk edits resolved by dropping the import,
breaking collection with `NameError: name 'pytest' is not defined` on every
runner. Re-add the import in the third-party group.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(integration): fix Windows UNC path assertion in status helper test
`test_strip_extended_length_prefix_normalizes_windows_paths` compared the
str() form of the helper's output against a hand-built string. On Windows,
pathlib renders a UNC root with a trailing separator (`\\server\share\`),
so the exact string match failed there (`\\server\share\` != `\\server\share`)
even though `_strip_extended_length_prefix` behaves correctly — the trailing
separator is irrelevant to the `relative_to` containment check it feeds.
Compare Path objects (semantic equality) instead of exact strings so the
assertion holds on both POSIX and Windows. No production code change needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(integration): make shared-manifest remediation specify --integration
The fallback `_manifest_suggestion` for the shared `speckit` manifest (used
when no usable default integration is recorded) suggested
`specify init --here --force`, which can trigger interactive integration
selection. For CI/agent consumers of `integration status`, surface an
explicit `--integration <key>` placeholder, matching the file's existing
`<key>` suggestion style.
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>
* chore: bump version to 0.10.0
* chore: begin 0.10.1.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat(init)!: make git extension opt-in and remove --no-git at v0.10.0
- Remove --no-git parameter from specify init command
- Remove git extension auto-installation from init flow
- Git repository initialization (git init) still runs when git is available
- Remove --no-git from all test invocations across the test suite
- Update docs to reflect opt-in git extension behavior
- Replace TestGitExtensionAutoInstall with TestGitExtensionOptIn tests
BREAKING CHANGE: specify init no longer auto-installs the git extension.
Use `specify extension add git` to install it explicitly.
The --no-git flag has been removed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(scripts): remove git operations from core scripts
Git functionality is now entirely managed by the git extension.
Core scripts only handle directory-based feature creation and numbering.
- Remove has_git(), check_feature_branch(), git branch creation from core
- Simplify number detection to use only spec directory scanning
- Remove HAS_GIT output from get_feature_paths()
- Remove git remote fetching and branch querying
- Keep BRANCH_NAME output key for backward compatibility
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor: remove all git operations from core
- Remove is_git_repo() and init_git_repo() dead code from _utils.py
- Remove --branch-numbering from init command
- Remove git from 'specify check' (now extension-only)
- Update docs: git is optional prerequisite, check command description
- Fix tests to reflect no-git-in-core reality (fallback to main)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(scripts): remove directory scanning and branch fallback from core
Core scripts now resolve feature context exclusively from:
1. SPECIFY_FEATURE env var (set by git extension)
2. .specify/feature.json (persisted by specify command)
Removed find_feature_dir_by_prefix() and directory scanning heuristics —
these are the git extension's responsibility. Scripts error clearly when
no feature context is available.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: introduce feature_numbering, deprecate branch_numbering in init-options
- specify command template now reads feature_numbering (preferred) with
fallback to branch_numbering (deprecated) from init-options.json
- Git extension reads git-config.yml > feature_numbering > branch_numbering
- init now writes feature_numbering: sequential to init-options.json
- Deprecation warning emitted when branch_numbering is used as fallback
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: remove trailing whitespace in common.ps1
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(scripts): persist SPECIFY_FEATURE_DIRECTORY env var to feature.json
When SPECIFY_FEATURE_DIRECTORY is set, get_feature_paths() now writes the
value to .specify/feature.json so future sessions without the env var can
still resolve the feature directory. The write is idempotent — it skips
when the file already contains the same value.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: address review feedback — error messages and docs
- Update error messages in common.sh and common.ps1 to reference
SPECIFY_FEATURE_DIRECTORY instead of SPECIFY_FEATURE (which no longer
resolves feature directories)
- Fix get_current_branch comment (returns empty string, not error)
- Update upgrade.md to reference SPECIFY_FEATURE_DIRECTORY with correct
example paths
- Update local-development.md troubleshooting: replace stale 'Git step
skipped' row with actionable git extension guidance
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(scripts): harden feature.json persistence
- Use json_escape in printf fallback when jq is unavailable (common.sh)
- Replace utf8NoBOM encoding with UTF8Encoding($false) for PowerShell
5.1 compatibility (common.ps1)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(scripts): remove dead feature_json_matches_feature_dir functions
These guards are no longer needed since the branch-name validation they
protected against has been removed from check-prerequisites.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor(git-ext): rename create-new-feature to create-new-feature-branch
The git extension's script only creates the git branch — rename it to
reflect that responsibility. The core create-new-feature.sh/.ps1 handles
feature directory creation and feature.json persistence.
Also includes fixes from review feedback:
- common.sh: _persist_feature_json uses json_escape fallback
- common.ps1: Save-FeatureJson uses UTF8Encoding for PS 5.1 compat
- common.ps1: case-sensitive path stripping on non-Windows
- create-new-feature.sh/ps1: output both SPECIFY_FEATURE and
SPECIFY_FEATURE_DIRECTORY
- setup-tasks.sh: fix stale 'Validate branch' comment
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(tests): update references to renamed git extension scripts
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(tests): remove duplicate EXT_CREATE_FEATURE assignments
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Update preset-fiction-book-writing to community catalog
- Preset ID: fiction-book-writing
- Version: 1.5.0
- Author: Andreas Daumann
- Description: Spec-Driven Development for novel and long-form fiction. Replaces software engineering terminology with storytelling craft: specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports 8 POV modes, all major plot structure frameworks, 5 humanized-AI prose profiles, and exports to DOCX/EPUB/LaTeX via pandoc. V1.5.0: Support interactive, audiobooks, series, workflow corrections
* Add fiction-book-writing preset to community catalog
- Preset ID: fiction-book-writing
- Version: 1.6.0
- Author: Andreas Daumann
- Description: Added support for 12 languages, export with templates, cover builder, bio builder, workflow fixes
* Update presets/catalog.community.json
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* fixed update_at for fiction-book-writing preset
* Update README.md
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* fixed description for fiction-book-writing
* Update Fiction Book Writing to community catalog
- Preset ID: fiction-book-writing
- Version: 1.9.0
- Author: Andreas Daumann
- Description: Update added illustration support
* 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: Copilot <175728472+Copilot@users.noreply.github.com>
* feat(extensions): per-event hook lists with priority ordering
The manifest validator restricted each hook event to a single mapping,
even though HookExecutor stores entries as a list per event. This blocked
an extension from running multiple commands on one event (e.g. a
verification step plus a doc-generation step after speckit.plan), and
get_hooks_for_event returned entries in raw insertion order with no way
to influence execution order across or within extensions.
This change:
1. Validator: accept hooks.<event> as either a single mapping or a list
of mappings. Each entry is validated individually and may carry an
optional integer `priority` (>= 1, default 10; bool rejected).
2. Command-ref normalization: apply rename / alias->canonical rewriting
to every entry in the list, not just the head.
3. register_hooks: expand list entries, persist `priority`, and
purge-and-replace all entries owned by the extension on each event so a
reinstall whose shape changed (single<->list, or a shorter list) leaves
no orphaned entries behind.
4. get_hooks_for_event: sort enabled entries by `priority` ascending with
a stable sort (ties keep insertion order). The existing
normalize_priority helper is reused as the sort key so corrupted
on-disk values fall back to the default instead of raising.
Backward compatible: existing single-mapping manifests parse and register
unchanged with priority defaulting to 10. The extension-level `priority`
used by preset/template resolution is independent of the new hook-entry
`priority`.
Implements #2378
* fix(extensions): harden register_hooks per PR review
- Skip non-dict hook entries before .get() so a manifest that bypasses
validation can't crash register_hooks with AttributeError.
- Normalize `priority` on save via normalize_priority so the on-disk
config stays clean, mirroring the read-side defense in
get_hooks_for_event.
- Tests: cover the non-dict-entry skip and add encoding="utf-8" to the
new tests' manifest writes.
* fix(extensions): purge dropped-event hook orphans on reinstall
register_hooks only purged events the new manifest still declared, so an
extension that dropped an event on reinstall left stale entries for it in
the project config. Purge this extension's entries from undeclared events
(and prune emptied events) before registering; scoped to this extension,
and a no-op for the install/update flow where unregister_hooks runs first.
* fix(extensions): reject boolean priority and complete orphan purge
- normalize_priority falls back to default for bool values
- dedup deletes duplicate commands before re-insert for last-wins ties
- register_hooks purges orphans even when all hooks are dropped
* docs(extensions): document per-event hook lists and priority
- EXTENSION-API-REFERENCE: hook event accepts a mapping or list; add
priority field reference and last-wins dedup note
- EXTENSION-DEVELOPMENT-GUIDE: add list-form example with priority
* docs(extensions): show both single and list hook forms in schema snippet
* docs(extensions): reference DEFAULT_HOOK_PRIORITY in normalize_priority
normalize_priority hard-coded the default as the literal 10 in both its
signature and docstring, duplicating DEFAULT_HOOK_PRIORITY. Reference the
constant in the signature and drop the literal from the docstring so the
default has a single source of truth.
* Initial plan
* feat!: remove legacy --ai, --ai-commands-dir, and --ai-skills flags at 0.10.0
* refactor(tests): rename stale test_ai_help_* methods to test_agent_config_*
* fix: address review — derive agent folder for generic integration and remove redundant test
- Security notice now falls back to integration_parsed_options['commands_dir']
when AGENT_CONFIG folder is None (generic integration).
- Remove test_agent_config_includes_kiro_cli which duplicates the assertion
in test_runtime_config_uses_kiro_cli_and_removes_q.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: scrub all remaining --ai flag references from source and tests
- Remove dead AI_ASSISTANT_ALIASES, AI_ASSISTANT_HELP, and
_build_ai_assistant_help() from _agent_config.py
- Update comments/docstrings in extensions.py, presets.py, and
integration subpackages to reference 'skills mode' or
'--integration' instead of the removed flags
- Fix catalog.json generic integration description
- Update test docstrings/comments in test_extension_skills.py,
test_extensions.py, and test_presets.py
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: remove legacy --ai flag rejection tests
The flags are fully removed from the CLI; typer handles unknown options
generically. No custom rejection logic exists to test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* revert: remove manual CHANGELOG.md entry
CHANGELOG is generated automatically; manual edits should not be made.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: make generic catalog description self-explanatory
Include the required --commands-dir sub-option in the description so
readers don't need to look up integration docs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(tests): rename duplicate test classes to avoid shadowing
The rename from Test*AutoPromote to Test*Integration collided with the
existing Test*Integration(SkillsIntegrationTests) base classes, causing
the shared test suites to be silently overwritten. Rename the CLI init
flow classes to Test*InitFlow instead.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: bump version to 0.9.5
* chore: begin 0.9.6.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat(extensions): add bundled bug triage workflow extension (#2870)
Add a bundled 'bug' extension providing a three-stage bug triage workflow:
- speckit.bug.assess: triage a bug report (pasted text or URL), locate
suspected code paths, and propose a remediation
- speckit.bug.fix: apply the proposed remediation and record what changed
- speckit.bug.test: validate the fix and record the verification result
Each bug gets its own directory under .specify/bugs/<slug>/ with one
Markdown report per stage (assessment.md, fix.md, test.md). The slug is
the only handle the three commands share; existing bug directories are
never overwritten.
Mirrors the layout of the existing bundled extensions (git, agent-context):
- extensions/bug/extension.yml, README.md, commands/
- extensions/catalog.json: register 'bug' (alphabetical, between
agent-context and git)
- pyproject.toml: add wheel mapping to specify_cli/core_pack/extensions/bug
Closes#2870
* address Copilot review on #2871
- speckit.bug.assess.md: drop POSIX-specific 'mkdir -p' example;
reword the prerequisite to describe the requirement (ensure BUG_DIR
exists) without assuming a specific shell.
- speckit.bug.fix.md: fix the slug-resolution fallback wording. It
listed '.specify/bugs/*/assessment.md' but then keyed off whether
'exactly one bug directory' existed; now it correctly keys off whether
exactly one matching 'assessment.md' was found and uses the slug from
its parent directory.
- tests/extensions/bug/test_bug_extension.py: add a smoke test analogous
to the agent-context extension's coverage. Validates the bundled
layout, catalog registration, '_locate_bundled_extension("bug")'
resolution, and that 'ExtensionManager.install_from_directory' installs
the three commands.
All 333 tests in tests/extensions/, tests/test_extensions.py, and
tests/test_extension_registration.py pass.
* address Copilot review on #2871 (round 2)
- Import _locate_bundled_extension from the public 'specify_cli'
package (it is re-exported in __init__.py) instead of the private
'specify_cli._assets' module, so the test does not depend on internal
module layout.
- Clarify module docstring: install_from_directory is called with
register_commands=False, so commands are copied and recorded in the
installed manifest but not registered with AI agents. Wording updated
to avoid implying otherwise.
* address Copilot review on #2871 (round 3)
- tests/extensions/bug/test_bug_extension.py: read extension.yml as
UTF-8 explicitly to avoid platform-dependent default encoding (notably
on Windows). Matches how the README is read in the same module.
- extensions/bug/commands/speckit.bug.assess.md: add a 'Safety When
Fetching URLs' section. Instructs the agent to treat fetched page
content as untrusted input (no obeying embedded prompt-injection
directives), forbids supplying credentials/secrets that a page asks
for, scopes the fetch to the URL the user provided (no following
redirects to other resources), and requires suspicious content to be
quoted verbatim under an 'Unverified' heading rather than acted on.
- extensions/catalog.json: bump 'updated_at' to today (2026-06-05) so
consumers that cache by this field invalidate when 'bug' is added.
- extensions/bug/README.md: minor grammar fix ('a reproduction that was
not actually performed').
All 251 tests in tests/extensions/bug/, tests/test_extensions.py, and
tests/test_extension_registration.py pass.
* speckit.bug.assess: add URL Trust Policy for fetched bug-report URLs
Builds on the 'Safety When Fetching URLs' section by adding a tiered
classification rule the agent applies before any fetch:
1. Refuse outright (no fetch, no prompt) for non-http(s) schemes,
loopback, link-local, RFC1918 private space, and known cloud
instance-metadata endpoints (169.254.169.254, metadata.google.internal,
100.100.100.200, metadata.azure.com). This closes the SSRF /
internal-recon vector opened by 'paste any URL'.
2. Fetch silently for an explicit allowlist of widely-used public
bug-report sources (github, gitlab, bitbucket, atlassian.net, linear,
stackoverflow/stackexchange, sentry). This preserves the paste-a-URL
ergonomics the workflow is built for.
3. Otherwise prompt once in interactive mode (default 'no', naming the
resolved host explicitly); in automated mode skip the fetch and
record '[UNVERIFIED - fetch skipped: host not on safe list: <host>]'
in assessment.md so a human can decide later.
In every case, assessment.md records the verbatim URL, the resolved host,
and which branch of the policy was taken (allowlisted /
confirmed-by-user / auto-refused: <reason>) so the per-bug directory's
audit trail is complete. Preflight HEAD probes are explicitly forbidden
since the probe itself is the request the policy gates.
Execution step 1 now defers to the policy before fetching.
* speckit.bug.assess: remove 'post-redirect-resolution' inconsistency
The URL Trust Policy explicitly forbids following redirects, but the
audit-trail bullet asked the agent to record the host
'post-redirect-resolution', which contradicted that rule and could lead
agents to follow redirects unintentionally to determine what to log.
Reword both call sites to refer to the host parsed from the URL the user
supplied (no resolution implied):
- Tier-3 interactive prompt: '...naming the host parsed from the URL
explicitly...'
- Recorded fields: 'The host parsed from that URL (no redirect following
- see the rule above).'
No behavior change; clarification only.
* fix: resolve GitHub release asset API URL for private repo preset and workflow downloads
- Add shared `resolve_github_release_asset_api_url` utility to `_github_http.py` for
reuse across preset and workflow download paths
- Apply the same private-repo fix from PR #2792 (extensions) to:
- `PresetCatalog.download_pack` — ZIP downloads via catalog `download_url`
- `preset add --from <url>` — ZIP downloads from a direct URL
- `workflow add <url>` — workflow YAML downloads from a direct URL
- `workflow add <id>` (catalog) — workflow YAML downloads via catalog `url`
- For browser release URLs (`github.com/…/releases/download/…`), the asset is
resolved via the GitHub REST API and downloaded with `Accept: application/octet-stream`
- Direct REST API asset URLs (`api.github.com/…/releases/assets/<id>`) are
downloaded directly with `Accept: application/octet-stream`
- Auth is preserved end-to-end through the existing `open_url` infrastructure
- Update `test_download_pack_sends_auth_header` and add
`test_download_pack_accepts_direct_github_rest_asset_url` to cover both paths
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: URL-encode tag in release API URL to handle special characters
Encode the tag as a path segment (using quote with safe='') when
building the releases/tags/<tag> API URL. This prevents malformed
URLs when tags contain reserved characters like '/' or '#'.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: add CLI-level tests for preset add --from GitHub release URL resolution
Adds regression tests covering:
- resolve_github_release_asset_api_url unit tests (passthrough, resolution,
network error, URL encoding of special chars in tags)
- CLI-level 'preset add --from <github-release-url>' end-to-end flow
- CLI-level 'preset add --from <api-asset-url>' direct passthrough
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* refactor: deduplicate release URL resolution; fix test issues
- ExtensionCatalog._resolve_github_release_asset_api_url now delegates
to the shared helper in _github_http.py (also gains URL-encoding fix)
- Remove unused 'io' import from test_github_http.py
- Remove duplicate 'provides' dict keys accidentally added to test_presets.py
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: align resolver timeout with download timeout; add workflow CLI tests
- Pass timeout=30 to resolve_github_release_asset_api_url in both
workflow add paths so worst-case latency matches the download timeout
- Add CLI-level regression tests for 'workflow add <url>' covering
browser URL resolution and direct API asset URL passthrough
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: remove unused urllib.request import; add catalog workflow test
- Remove unused 'import urllib.request' in preset add --from path
- Add CLI test for catalog-based 'workflow add <id>' with GitHub
release URL resolution
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* style: remove unused MagicMock imports from tests
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 <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(workflows): render gate show_file contents in the interactive prompt
The gate step read and recorded `show_file` but never displayed its
contents at the interactive prompt, so the operator approved/rejected
without seeing the referenced file. Render the file inside the prompt
when stdin is a TTY, with a graceful notice for missing/unreadable
files. Non-interactive PAUSED behaviour, exit codes, resume semantics,
and no-`show_file` output are unchanged.
Closes#2809.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): keep gate _prompt signature stable and harden show_file reads
The gate prompt rendered show_file by passing it as a third positional
argument to _prompt. A test that stubs _prompt with a two-argument lambda
(test_gate_abort_still_halts_with_continue_on_error) then failed once the
branch caught up to main, because the call site passed three arguments to
the two-argument stub.
Compose the show_file material into the displayed message in execute() and
keep _prompt to its (message, options) contract. Display data no longer
widens the interactive seam, so stubbing _prompt stays stable and future
review material can be added without breaking callers. _prompt now renders
a multi-line message inside the gate box.
Also catch ValueError in _read_show_file so a path the OS rejects outright
(e.g. an embedded NUL byte) degrades to a notice instead of crashing the
prompt, matching the helper's stated contract.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): coerce gate prompt message to str before rendering
The multi-line render loop split the message on newlines, which assumes a
str. A non-string message (e.g. a YAML numeric literal) previously rendered
fine through the old f-string and would now raise on .split. Coerce with
str() to preserve that tolerance, and add a regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(workflows): make gate stdin handling robust; tidy compose_prompt typing
Address review feedback on the gate tests and helper:
- Swap the gate module's sys.stdin for a fixed-isatty stub (shared
_StubStdin / _force_gate_stdin helpers) instead of setattr on
sys.stdin.isatty, which is not assignable under some pytest capture
modes. This also forces the non-interactive tests to a non-TTY so they
cannot block on input() when run in a real terminal.
- The non-interactive show_file test now hard-fails if _read_show_file is
called, proving the file is not read on the PAUSED path.
- _compose_prompt accepts a non-string message (e.g. a YAML numeric
literal) and always returns str via str(message), keeping its annotation
and docstring accurate; the redundant coercion in _prompt is removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): strip control chars from gate show_file; default tests non-TTY
Address review feedback:
- _read_show_file strips C0 control characters (except tab) from each line,
so a show_file containing ANSI escape sequences (e.g. \x1b[2J) cannot
clear the screen or spoof the prompt/options when rendered to a terminal.
- Add an autouse fixture on TestGateStep that defaults every gate test to a
non-TTY stdin, so no test can drop into the interactive prompt and block
on input() when the suite runs under a real TTY. Interactive tests opt
back in via _force_gate_stdin(tty=True); the now-redundant explicit
non-TTY calls were removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(workflows): localize gate stdin patch to the gate module's sys
_force_gate_stdin rebinds the gate module's `sys` name to a stand-in whose
stdin has a fixed isatty() and which delegates every other attribute to the
real sys, instead of mutating the process-wide sys.stdin. This keeps the
patch local to the gate module and leaves real stdin untouched. The gate
abort test, which used the same process-wide swap, now shares the helper, so
the pattern exists in exactly one place.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): sanitize the displayed gate show_file path, not just content
Control characters were stripped from show_file *contents* but the path was
still printed verbatim as the header (`f"{show_file}:"`) and echoed in the
read-error notice, so a show_file path containing ANSI escapes could still
inject terminal sequences. Centralize stripping in `_sanitize_for_display`
and apply it to every show_file-derived string that reaches the terminal —
the displayed path, each file line, and the error notice — while still
opening the file with the original path. Add a test for path sanitization.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(workflows): inline control-char stripping, drop the helper
Reuse the existing _CONTROL_CHARS regex directly at the three display sites
instead of wrapping it in a one-line helper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): also strip LF and C1 controls from gate show_file display
The control-char class skipped LF (so an embedded newline in a show_file
path could break the boxed layout) and the C1 range (so \x9b CSI and other
8-bit controls survived). Widen the class to [\x00-\x08\x0a-\x1f\x7f-\x9f]
(still keeping tab).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: add support for rovodev
* fixup! feat: add support for rovodev
* fixup! feat: add support for rovodev
* fixup! feat: add support for rovodev
* fixup! feat: add support for rovodev
* fixup! feat: add support for rovodev
* fixup! feat: add support for rovodev
* fixup! feat: add support for rovodev
* fixup! feat: add support for rovodev
* fixup! feat: add support for rovodev
* fixup! feat: add support for rovodev
* fixup! feat: add support for rovodev
* chore: bump version to 0.9.4
* chore: begin 0.9.5.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat(workflows): add --json output to workflow run, resume, and status
Adds an opt-in `--json` flag to `workflow run`, `workflow resume`, and
`workflow status` that emits a single machine-readable object (run_id,
workflow_id, status, current step; status also reports per-step states
and a runs list) for automation and external orchestrators.
JSON is written via a small `_emit_workflow_json` helper using plain
stdout, so Rich markup, highlighting, and line-wrapping can never alter
the emitted object. Default human-readable output and exit codes are
unchanged when `--json` is omitted. Reference docs updated.
Closes#2811.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(workflows): keep --json stdout clean while steps write output
Suppressing the banner and the step-start callback was not enough to
guarantee a single parseable JSON object on stdout: individual steps still
write there while the engine runs. The gate step prints its prompt, and the
prompt step runs a CLI subprocess that inherits the process's stdout file
descriptor — either can corrupt the JSON stream for interactive runs or
integration-backed workflows.
Wrap engine.execute()/engine.resume() in a file-descriptor-level redirect
(dup2) when --json is set, so both Python-level writes and inherited-fd
subprocess output go to stderr while stdout carries only the emitted JSON.
Step progress stays visible on stderr. status does not run the engine, so
it is unaffected.
Tests cover both pollution channels (a Python print and a real subprocess)
via fd-level capture, and the inactive no-op path. Docs note the
stdout/stderr split.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(workflows): fix stray escape sequence in --json redirect comments
The redirect helper's docstring and its test comment wrote ``print``\s,
which renders as "print\s" rather than "prints". Replace with plain
"prints".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extension command registration now resolves the active skills directory before writing command artifacts. This lets initialized skills-backed agents recover a missing active skills directory while preserving the existing preset registration behavior.
Add regression coverage for missing active skills directories, shared skills directories, and symlinked parent guards.
Fixes#2769.
Co-authored-by: OpenAI Codex <codex@openai.com>
* fix(cursor-agent): enable CLI dispatch via ``-p --trust`` headless mode
Restores the ability for ``specify workflow run`` to dispatch the
cursor-agent CLI, complementing the existing in-IDE skill flow.
Without this fix, ``specify workflow run speckit --input
integration=cursor-agent ...`` fails with a misleading
``CLI not found or not installed`` error even when the CLI is
installed (since cursor-agent had ``requires_cli=False`` and an
unset ``build_exec_args``).
The cursor-agent CLI (>= 2026.05.16) supports headless execution
via ``-p`` (print mode with full tool access including write/shell)
and ``--trust`` (bypass Workspace Trust prompt). Without ``--trust``
the CLI exits non-zero in non-TTY contexts (verified locally).
Changes to ``src/specify_cli/integrations/cursor_agent/__init__.py``:
* ``config.requires_cli``: ``False`` -> ``True``
* ``config.install_url``: ``None`` -> Cursor CLI docs URL
* Override ``build_exec_args()`` to emit
``[cursor-agent, -p, --trust, <prompt>, ...]``
with optional ``--model`` and ``--output-format json`` flags,
mirroring the shape used by ``claude``/``codex``/``gemini``.
Tests:
* 34 existing cursor-agent tests still pass.
* 6 new tests in ``TestCursorAgentCliDispatch`` pin
``requires_cli``, ``install_url``, and the exact argv shape
(default, text-output, with-model, and the hyphenated skill
invocation form ``/speckit-<name>``).
* Full repo: 1085 / 1085 passed, no regressions.
Fixes#2629
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(integrations): resolve ``.cmd``/``.bat`` shims before subprocess.run
On Windows, ``shutil.which`` honors ``PATHEXT`` and locates wrappers
like ``cursor-agent.cmd`` and ``codex.cmd``, but Python's
``subprocess.run`` calls ``CreateProcess`` which does **not** consult
``PATHEXT`` and therefore fails with ``WinError 2`` on a bare argv
like ``[cursor-agent, ...]``.
Resolve ``exec_args[0]`` via ``shutil.which`` in
``IntegrationBase.dispatch_command`` so ``.cmd``/``.bat`` shims work
transparently. On POSIX this is a no-op for absolute paths and a
harmless lookup otherwise.
Verified locally on Windows 10 + cursor-agent 2026.05.16:
without this fix, ``specify workflow run speckit --input
integration=cursor-agent`` fails with ``FileNotFoundError`` even
after the cursor-agent integration starts producing valid exec
args (per the prior commit on this branch).
Tests:
* New: 2 cursor-agent tests pin the shim-resolution + passthrough
behavior (``test_dispatch_command_resolves_cmd_shim_for_subprocess``
and ``test_dispatch_command_passthrough_when_shutil_which_finds_nothing``).
* Updated: ``tests/test_workflows.py::TestCommandStep::test_dispatch_with_mock_cli``
was mocking ``shutil.which`` only at the ``command`` step level
and not at the ``base`` level, which made it environment-sensitive
(fails locally when the real ``claude`` CLI is on PATH). Added the
matching base-level patch and updated the argv-assertion to reflect
the resolved path. ``test_dispatch_failure_returns_failed_status``
gets the same patch for consistency.
* Full repo: 2867 passed, 0 regression from this PR. The 12 remaining
pre-existing failures are unrelated Windows ``symlink`` privilege
failures (``WinError 1314``) on a non-admin Windows runner.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cursor-agent): inject --approve-mcps --force for headless MCP/tool access
The previous commit (1c55988) wired up ``-p --trust`` so the CLI launches
in headless mode without the Workspace Trust prompt, but that alone is
not enough to let ``specify workflow run`` drive a real speckit feature
end-to-end with cursor-agent on Windows. Two more flags are required:
* ``--approve-mcps``: without it, every MCP server configured in
``.cursor/mcp.json`` stays ``not loaded (needs approval)``, and any
tool call against them is silently dropped. We hit this immediately
trying to read a DingTalk PRD from a remote MCP server during the
``/speckit-specify`` step.
* ``--force``: without it, the agent halts on the first tool-call
approval prompt (the tool call gets rejected and the workflow exits
non-zero with a misleading message). With ``--force`` cursor-agent
matches the implicit "trusted environment" semantics that ``claude -p``
and ``codex --exec`` already have by default -- which is the right
semantics for an unattended ``specify workflow run`` invocation.
Verified end-to-end on Windows 10 + cursor-agent 2026.05.16-0338208:
* ``cursor-agent -p --trust --approve-mcps --force --output-format text``
+ a ``/speckit-specify`` prompt that included a DingTalk URL produced
a full spec.md (31.5 KB) plus checklists/requirements.md in ~10.7 min,
reading the source PRD through the ``dingtalk-doc`` remote MCP server,
deciding the ``specs/`` subpath itself, and updating
``.specify/feature.json`` and ``specs/menu-dictionary.md`` along the
way -- no human-in-the-loop, no source PRD ever touched the filesystem.
* Without ``--approve-mcps`` the same prompt errors with the tool call
rejected message; without ``--force`` the agent stops at the first
non-MCP tool call.
Tests:
* ``test_build_exec_args_*`` updated to pin the new four-flag prefix.
* New ``test_build_exec_args_contains_mandatory_headless_flags`` asserts
the four flags are always present together.
* ``test_dispatch_command_resolves_cmd_shim_for_subprocess`` updated to
match the new argv layout.
* All 43 cursor-agent tests pass; no other tests touched.
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(cursor-agent): express dispatch support via build_exec_args() instead of requires_cli
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(cursor-agent): use urlparse hostname check and cover dispatch without requires_cli
Co-authored-by: Cursor <cursoragent@cursor.com>
* Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: 刘一 <liuyi@oureman.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Update Superpowers Implementation Bridge extension to v1.0.2
Update speckit-superpowers-bridge extension submitted by @lihan3238:
- extensions/catalog.community.json (version, download_url, updated_at)
The download URL now uses the stable latest-release alias
(speckit-superpowers-bridge.zip) per the maintainer's distribution policy.
Closes#2848
* Pin speckit-superpowers-bridge download_url to v1.0.2
Use the version-pinned release asset URL instead of the
releases/latest/download alias so the catalog entry tracks the
specific version declared in the entry rather than silently
following future releases. Matches the pinning convention used
by other entries in the catalog.
* docs(agents): add PR review response guidance to prevent comment flooding
Adds a 'Responding to PR Review Comments' section to AGENTS.md so agents
acting on PRs stop posting one reply per review comment. Directs them to
post one summary comment per review round, disclose their identity and
the human they're acting for, never click 'Resolve conversation', and
re-request review once per round rather than after every push.
Closes#2849
* 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: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Initial plan
* feat: add --workflow option to init command for post-init workflow execution
* chore: remove unused import in test file
* refactor: allow workflow run without project when given a YAML file path
Instead of adding --workflow to init, make `specify workflow run ./file.yml`
work without requiring a .specify/ project directory. When the source is a
YAML file that exists on disk, cwd is used as the project root. When it's a
workflow ID, the .specify/ project requirement is preserved.
* Handle standalone workflow path edge cases
* Fix USERPROFILE env var portability and docs notation
* Fix workflow YAML path detection to require regular files
* Harden workflow run against unsafe .specify paths
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* feat(extensions): add --force flag to extension add for overwrite reinstall
Add --force support to `specify extension add` that allows overwriting
an already-installed extension without manually removing it first.
- install_from_directory() and install_from_zip() accept force=True,
automatically calling remove() before installation
- The --force CLI flag works with all install modes (--dev, --from URL,
bundled, and catalog)
- Config files (*-config.yml) are preserved across force reinstall
- Error message suggests --force when extension is already installed
- 6 new tests covering unit and CLI force reinstall flows
* fix: address PR review feedback on --force implementation
- Remove unused `backup_config_dir` variable assignment (Ruff F841)
- Defer `remove()` until after `_validate_install_conflicts()` to prevent
data loss if validation fails mid-reinstall
- Use `TemporaryDirectory` instead of `NamedTemporaryFile` in ZIP test
to avoid Windows file-locking failures
* fix: only restore config backup when --force actually triggers a remove
When --force is used but the extension is not already installed, the
backup restore/cleanup should not run. Previously it could resurrect
stale config files from a previous removal and delete the backup
directory unnecessarily.
* fix: address Copilot review feedback on --force implementation
- Clear stale backup dir before remove() so only fresh backups are restored
- Restore only config files (*-config.yml, *-config.local.yml) from backup
- Remove trailing \n from --force console message (console.print adds newline)
* fix: handle non-directory paths in backup cleanup/restore
- Use is_dir() before rmtree/iterdir on backup path to avoid crashes
when .backup/<id> exists as a file or symlink
- Remove unused manifest1 variable in test_install_force_reinstall
* fix: handle symlinks in backup cleanup/restore and correct CLI message
- Check is_symlink() before is_dir() in backup cleanup and restore:
Path.is_dir() follows symlinks (returns True for symlink-to-dir) but
shutil.rmtree() raises OSError on symlinks. Handle symlinks by
unlinking them instead.
- Skip symlink entries during config file restore.
- Change --force dev-install message from "Reinstalling" to
"Installing [...] (will overwrite if already installed)" because
--force also works for first-time installs.
* chore: bump version to 0.9.3
* chore: begin 0.9.4.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Clear pre-existing lint debt flagged by repo-wide `ruff check` (the lint
config only scopes src/, so tests/ had drifted). No behavior change.
- F401/F541: drop unused imports and redundant f-string prefixes (autofix)
- E741: rename ambiguous `l` to `ln` in comprehensions
- E702: split semicolon-joined statements onto separate lines
- F841: drop unused bindings while keeping the side-effecting calls
(_minimal_feature, install_from_directory)
Full suite: 3344 passed, 40 skipped. ruff check (repo-wide): clean.
* fix(workflows): validate run_id in RunState.load before touching the filesystem
``RunState.load(run_id, project_root)`` interpolates ``run_id`` directly
into ``project_root / ".specify" / "workflows" / "runs" / run_id`` and
then calls ``state_path.exists()`` and ``json.load`` on the result. The
run_id is reachable from user input via ``specify workflow resume
<run_id>`` (CLI argument) and via ``SPECKIT_WORKFLOW_RUN_ID`` (env var
override on the engine's run path), so a value like ``../escape``
turns ``runs_dir`` into ``.specify/workflows/escape/`` and:
* ``state_path.exists()`` becomes a file-existence oracle for any
path the process can read.
* if a ``state.json`` exists at the traversed location (planted by
a malicious dependency, a misconfigured shared workspace, or an
older spec-kit version that happened to write there),
``json.load`` parses it and the workflow resumes under the
attacker-chosen ``workflow_id`` / step state.
* a subsequent ``state.save()`` then writes back to the traversed
location, persisting the corruption.
``RunState.__init__`` already validates ``run_id`` against
``r'^[a-zA-Z0-9][a-zA-Z0-9_-]*$'`` — but that check runs on
``state_data["run_id"]`` *after* ``load`` has already done the file
lookup, which is too late to prevent the disclosure.
This change extracts the pattern into a class-level constant
``_RUN_ID_PATTERN`` and a single ``_validate_run_id`` classmethod so
``__init__`` and ``load`` cannot drift, then calls the validator at the
top of ``load`` before any path is built. Mirrors the precedent in
``src/specify_cli/agents.py::_ensure_within_directory`` (used at line
437 of that file) which guards extension-install paths against the
same threat model.
Regression tests parametrize 9 traversal vectors (``../escape``,
``..``, ``../../etc/passwd``, ``foo/bar``, ``foo\bar``, ``.hidden``,
``-flag``, ``foo\x00bar``, empty) and plant a malicious ``state.json``
outside ``runs/`` so a missing guard would surface as a successful
load rather than the ambiguous ``FileNotFoundError``. A second test
asserts ``__init__`` and ``load`` reject the same representative
malformed ID, so future changes to one path can't silently drift from
the other.
* test(workflows): exercise RunState.load in shared-validation test, fix __init__ empty-string asymmetry
Copilot's review on this PR pointed out that
test_init_and_load_share_validation claimed to verify both entry
points share the same validation rules but never actually called
RunState.load — only __init__ and the shared
_validate_run_id helper. A regression in load (e.g. someone
deleting the cls._validate_run_id(run_id) call before the path is
built) would slip through even though __init__ and the helper
stayed aligned, defeating the whole point of the test.
Tightening the test surfaced a real asymmetry the previous version was
silently masking:
self.run_id = run_id or str(uuid.uuid4())[:8]
The truthiness fallback meant RunState(run_id="") silently
substituted a UUID and skipped validation, while
RunState.load("", project_root) correctly rejected the empty
string. The two entry points diverged on the empty-string vector.
That is exactly the drift the test name claimed to defend against —
and the original test missed it.
Changes
-------
* engine.py: __init__ now distinguishes run_id is None
(caller omitted it → auto-generate UUID) from an empty string
(caller provided it → must validate like any other value). Both
paths still flow through _validate_run_id, but only the
explicit-None case auto-generates.
* test_workflows.py: test_init_and_load_share_validation is
now parametrized over one representative vector per category from
test_load_rejects_path_traversal (parent traversal, embedded
separator, leading non-alphanumeric, empty string) and asserts that
*all three* entry points — __init__, _validate_run_id, and
load — reject the same input. Adding load to the assertion
is the substantive fix Copilot asked for; keeping __init__ and
the helper alongside it makes any future drift between the three
immediately observable instead of having to read three separate
tests.
Verification
------------
pytest tests/test_workflows.py — 168 passed (was 165 before the
parametrize expansion; __init__ empty-string vector would have
failed the new test against the old engine code, confirming the
asymmetry was real).
* feat(cli): implement specify self upgrade
* fix(cli): normalize self-upgrade prerelease tags
* fix(cli): tighten self-upgrade diagnostics
* fix(cli): harden self-upgrade verification parsing
* fix(cli): sanitize self-check fallback tags
* fix(cli): harden self-check release display
* fix(cli): validate resolved upgrade tags
* fix(cli): tolerate invalid install metadata
* test(cli): align upgrade network mocks
* fix(cli): respect relative installer paths
* fix(cli): tighten upgrade failure handling
* fix(cli): align installer path diagnostics
* fix(cli): validate release and version output
* fix(cli): clarify source checkout guidance
* fix(cli): harden upgrade detection helpers
* fix(cli): avoid echoing invalid release tags
* fix(cli): tolerate argv path resolve failures
* chore: remove self-upgrade formatting-only diffs
* fix: address self-upgrade review feedback
* fix: address self-upgrade review followups
* fix: address self-upgrade review edge cases
* fix: address self-upgrade review docs
* fix: refine self-upgrade review followups
* fix: address self-upgrade review cleanup
* fix: handle self-upgrade review edge cases
* fix: address self-upgrade review nits
* fix: address follow-up self-upgrade review
* fix: resolve self-upgrade review and Windows CI failures
- README: promote "Optional Commands" to ### so it is a sibling of
"Core Commands" under "Available Slash Commands" (consistent heading
levels; avoids the h2->h4 jump a revert would create).
- _version: allow --tag prerelease/dev and build-metadata suffixes to
compose (e.g. v1.0.0-rc1+build.42), matching PEP 440 / semver; the
Version() check still enforces canonical validity.
- tests: compare resolved argv0 as Path objects instead of POSIX strings
so the assertion holds on Windows; skip the relative-installer-path
executable-bit tests on Windows via a new requires_posix marker (they
rely on chmod/X_OK semantics and chdir-into-tmp teardown that do not
hold there). Add a combined prerelease+build-metadata tag test.
* fix: address second self-upgrade review round
- self_check: clarify that the "up to date" branch is reached only for
parseable latest tags (the unparseable case returns earlier), so the
InvalidVersion fallback assumption is not reintroduced.
- self_upgrade: compare target/current as Version instances directly
instead of re-parsing the canonical strings through _is_newer; the
empty-current case stays explicit via the not-None guard.
- tests: document the intentional broad GH_/GITHUB_ env scrub with a test
asserting non-credential context vars (GH_HOST, GITHUB_REPOSITORY, …) are
stripped from the installer subprocess env — a deliberate fail-safe that
also catches credential-adjacent names without a recognized suffix.
* fix: address third self-upgrade review round
- self_upgrade: unify the no-op short-circuits on packaging Version
equality instead of canonical-string equality. Version("1.0") equals
Version("1.0.0") but their str() forms differ, so the old check could
misreport an equal install as "already on latest release or newer".
Both the unpinned and pinned branches now use Version comparison.
- self_upgrade: compare the verified version as a parsed Version against
the target so a non-version verifier result is a mismatch (exit 2)
rather than a coincidental canonical-string match.
- resolver: map HTTP 429 (Too Many Requests / secondary rate limit) to
the rate-limited category so users get the same actionable token hint
as 403.
- _is_github_credential_env_key: document the precise (intentionally
broad) scrub matching contract in the docstring.
- tests: add a trailing-zero Version-equality regression test and a
parametrized HTTP-status categorization test (429 -> rate limited;
404/502 -> verbatim).
* fix: address fourth self-upgrade review round
- self_upgrade: label a pinned target older than the installed version as
"Downgrading" rather than "Upgrading" so `--tag <older>` is not mistaken
for a forward upgrade.
- resolver: drop the unused `typing.Optional` import and annotate the
`--tag` option as `str | None`, consistent with the rest of the module
(verified Typer resolves it on the supported Python versions).
- _is_github_credential_env_key: add `_PASSWORD` and `_CREDENTIALS` to the
recognized credential suffixes and document that only these shapes are
scrubbed (not blanket coverage).
- tests: assert the precise exit code (1) for the re-raised transient
OSError path; skip the InvalidMetadataError test on Pythons where the
real exception is absent instead of fabricating it; update the pinned
downgrade test to expect the "Downgrading" label.
* fix: accept uppercase V prefix in --tag
Fold a leading uppercase `V` (a common paste) to the canonical lowercase
`v` before validating `--tag`. The remainder of the tag stays
case-sensitive on purpose: the validated value is used verbatim as a git
ref, which is case-sensitive on GitHub, so rewriting label/build-metadata
casing could point at a tag that does not exist. Adds a normalization test.
`workflow resume` now accepts `--input key=value` (the same flag and
parsing as `workflow run`, via a shared `_parse_input_values` helper).
Supplied values are merged over the run's persisted inputs and
re-resolved through the existing typed-validation path
(`_resolve_inputs`), so a resumed/re-run step sees the updated inputs
and ill-typed values fail fast. Keys not supplied keep their persisted
values; resuming without `--input` is unchanged. Reference docs updated.
Distinct from #2405 (file-reference inputs at run time): this is about
supplying inputs at resume time, reusing the existing input model.
Closes#2812.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On Windows, when stdout/stderr are not a UTF-8 TTY (output piped, redirected
to a file, or running under a legacy code page such as cp1252), Rich cannot
encode the banner and box-drawing glyphs, so the CLI aborts with a
UnicodeEncodeError traceback instead of printing. This breaks basic commands
like `specify --help` and `specify version` whenever their output is captured
rather than written to an interactive terminal.
Reconfigure sys.stdout/sys.stderr to UTF-8 with errors="replace" at the
main() entry point on win32 so output degrades gracefully instead of crashing.
The change is a no-op on POSIX, is guarded by try/except so it can never make
stream setup worse, and lives at the CLI entry point only -- importing
specify_cli as a library does not touch global streams.
Verified on Windows 11 (cp1252): `specify --help` piped and `specify version`
redirected to a file both render correctly and exit 0 without setting
PYTHONUTF8 / PYTHONIOENCODING.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore: bump version to 0.9.2
* chore: begin 0.9.3.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 17:52:31 -05:00
445 changed files with 91728 additions and 9962 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)
**Category** — one of: `docs`, `code`, `process`, `integration`, `visibility`
**Effect** — `Read-only`(produces reports only) or `Read+Write` (modifies project files)
**Category** — free-form; common values: `docs`, `code`, `process`, `integration`, `visibility`
**Effect** — write canonical values`read-only`or `read-write` in `extension.yml` and `catalog.community.json`; use `Read-only`/`Read+Write` only for the docs table display
# Days of inactivity before an issue or PR becomes stale
days-before-stale:150
# 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
@@ -14,7 +14,7 @@ The toolkit supports multiple AI coding assistants, allowing teams to use their
Each AI agent is a self-contained **integration subpackage** under `src/specify_cli/integrations/<key>/`. The subpackage exposes a single class that declares all metadata and inherits setup/teardown logic from a base class. Built-in integrations are then instantiated and added to the global `INTEGRATION_REGISTRY` by `src/specify_cli/integrations/__init__.py` via `_register_builtins()`.
@@ -52,30 +52,30 @@ 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/",
"commands_subdir":"workflows",
"name":"Kilo Code",
"folder":".kilo/",
"commands_subdir":"commands",
"install_url":None,
"requires_cli":False,
}
registrar_config={
"dir":".windsurf/workflows",
"dir":".kilo/commands",
"legacy_dir":".kilocode/workflows",
"format":"markdown",
"args":"$ARGUMENTS",
"extension":".md",
}
context_file=".windsurf/rules/specify-rules.md"
```
**TOML agent (Gemini):**
@@ -101,7 +101,6 @@ class GeminiIntegration(TomlIntegration):
"args":"{{args}}",
"extension":".toml",
}
context_file="GEMINI.md"
```
**Skills agent (Codex):**
@@ -129,7 +128,6 @@ class CodexIntegration(SkillsIntegration):
"args":"$ARGUMENTS",
"extension":"/SKILL.md",
}
context_file="AGENTS.md"
@classmethod
defoptions(cls)->list[IntegrationOption]:
@@ -150,9 +148,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 +188,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`, `.ps1`, and `extensions/agent-context/scripts/python/update_agent_context.py`). 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 +202,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, .kilo/commands/)
ls -R my-project/.kilo/commands/
# Uninstall cleanly
cd my-project && specify integration uninstall <key>
@@ -270,6 +269,25 @@ echo "✅ Done"
## Command File Formats
### Script References (`scripts:` frontmatter)
Core command templates (`templates/commands/*.md`) that invoke a helper script declare it in a `scripts:` frontmatter block with one line per supported script type. The `{SCRIPT}` placeholder in the command body is replaced at install time with the entry matching the project's selected script type (`--script sh|ps|py`):
All three entries must be present and behaviorally equivalent — agents parse the same stdout contract (`FEATURE_DIR:…`, `AVAILABLE_DOCS:…`, `--json` shapes) regardless of which one runs. (The bundled `agent-context` and `git` extension command templates also invoke helpers but do not yet use `scripts:` frontmatter — see [Script Types and Migration](#script-types-and-migration).)
### Markdown Format
**Standard format:**
@@ -330,9 +348,29 @@ Different agents use different argument placeholders. The placeholder used in co
- **TOML-based**: `{{args}}` (e.g., Gemini)
- **YAML-based**: `{{args}}` (e.g., Goose)
- **Custom**: some agents override the default (e.g., Forge uses `{{parameters}}`)
- **Script placeholders**: `{SCRIPT}` (replaced with actual script path)
- **Script placeholders**: `{SCRIPT}` (replaced with the resolved command from the template's `scripts:` frontmatter, per the project's `--script sh|ps|py` selection)
- **Agent placeholders**: `__AGENT__` (replaced with agent name)
## Script Types and Migration
Spec Kit ships every core workflow script in three interchangeable variants — POSIX shell (`sh`), PowerShell (`ps`), and Python (`py`) — selected per project with `specify init --script sh|ps|py`. Each core command template that invokes a helper script carries all three in its `scripts:` frontmatter (templates that don't call a script, e.g. `constitution`/`specify`, have no `scripts:` block); see [Script References](#script-references-scripts-frontmatter).
### Why Python is recommended
- **No extra runtime.** The `specify` CLI is already Python, so the interpreter is guaranteed present — `py` adds no new dependency.
- **Path toward a single source of truth.** The shell variants require paired `.sh` + `.ps1` maintenance and diverge on JSON handling (`jq` vs manual parsing). The Python variant avoids `jq` and is intended to eventually replace that dual-maintenance — but that consolidation has not happened yet: all three variants are still maintained in parallel (see the parity rule below).
- **Parity-tested.** The Python ports are covered by tests — output-parity tests against the shell scripts where the contract is stdout-based, and direct unit tests elsewhere — so the stdout contract agents rely on stays stable.
### Defaults and availability
-`py` is available today for the core command templates (via their `scripts:` frontmatter). The bundled extensions (`agent-context`, `git`) ship Python script variants on disk, but their command templates still hard-code the Bash/PowerShell invocations, so `--script py` does not yet route those extension commands to Python — wiring `py` into the extension command templates is tracked separately.
- Selection is per project: interactive `specify init` prompts for the script type, while non-interactive runs default to a shell variant by OS (`sh` on Linux/macOS, `ps` on Windows). `py` is chosen at the prompt or via `--script py`.
-`sh` and `ps` remain fully supported. Nothing is removed, and `py` is not yet the default.
### Parity rule for contributors
All three script types are first-class: any change to a workflow script must update `sh`, `ps`, and `py` together and keep their tests (parity and unit) green. Making `py` the default and eventually retiring `sh`/`ps` is future work gated on adoption, tracked under the script-unification epic ([#3277](https://github.com/github/spec-kit/issues/3277)) — not something to act on from this doc.
## Special Processing Requirements
Some agents require custom processing beyond the standard template transformations:
@@ -340,18 +378,21 @@ Some agents require custom processing beyond the standard template transformatio
### Copilot Integration
GitHub Copilot has unique requirements:
- Commands use `.agent.md` extension (not `.md`)
- Each command gets a companion `.prompt.md` file in `.github/prompts/`
- Installs `.vscode/settings.json` with prompt file recommendations
- Context file lives at `.github/copilot-instructions.md`
Implementation: Extends `IntegrationBase` with custom `setup()` method that:
1. Processes templates with `process_template()`
2. Generates companion `.prompt.md` files
3. Merges VS Code settings
**Skills mode (`--skills`):** Copilot also supports an alternative skills-based layout
via `--integration-options="--skills"`. When enabled:
- Commands are scaffolded as `speckit-<name>/SKILL.md` under `.github/skills/`
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
Branches follow one of two patterns depending on whether an issue exists:
```
```text
<type>/<number>-<short-slug> # when an issue is created first
<type>/<short-slug> # when no issue exists (PR-only changes)
```
@@ -423,13 +467,47 @@ When an issue exists, include its number immediately after the prefix — this i
---
## Agent Disclosure for PRs, Comments, and Commits
Disclosure is **continuous**, not a one-time event. A single AI-disclosure paragraph in the PR body does **not** cover the commits and replies you add during review rounds. Each of the following must independently attest to agent authorship.
### Commits
- **Every commit you author must carry an `Assisted-by:` trailer** identifying the agent and whether it acted autonomously or under direct human supervision, for example:
Use `supervised` instead of `autonomous` only when a human actually authored or line-by-line reviewed the change before it was committed.
- **Never push solo-authored commits that hide agent authorship behind the operator's git identity.** If an agent generated the change, the trailer must say so even when the commit is attributed to a human account.
- Preserve any tool-generated `Co-authored-by:` trailers (e.g. Copilot Autofix) — do not strip them to make a commit look hand-written.
### Comments
- If you are an agent working on behalf of a human, **disclose your identity in your PR comment** — name the agent (and model, if applicable) and the human you are acting for (e.g., "Posted on behalf of @user by GitHub Copilot (model: <name-if-known>)").
- **Re-state agent identity in each review-round summary comment.** A prior PR-body disclosure does not cover later comments or commits.
- Post **one** top-level summary comment per review round listing what changed and the commit SHA. Do not reply on every individual comment.
- Reply inline only when context is needed (disagreement, deferral, non-obvious fix). Keep it to a sentence or two.
- **Never click "Resolve conversation"** — that belongs to the reviewer or PR author.
- No emoji, no celebratory framing, no checklist mirroring the reviewer's items, no restating what the reviewer wrote.
- Re-request review once per round (when all feedback is addressed), not after every intermediate push.
### Anti-patterns (do not do these)
- **Do not** reply "Done" or push a "fix" within seconds/minutes of a review event without disclosing that the response or commit was agent-generated. Speed of turnaround is not a substitute for attestation — a near-instant tested code change is itself a signal of automation and must be disclosed as such.
- **Do not** claim "reviewed, tested, and understood by me" for commits that were authored and pushed automatically in response to a review trigger. If the loop is automated, disclose it as automated.
---
## 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.
6. **Running tests against the wrong environment**: Always run the suite inside this working tree's own virtualenv (`uv sync --extra test` then `.venv/bin/python -m pytest`, or activate the venv first). A bare `uv run pytest` can resolve to an ambient/global interpreter whose editable `.pth` points at a *different* worktree. The failure is sneaky: test collection still imports `specify_cli` successfully, but newly-added subpackages (e.g. a fresh `specify_cli/bundler/`) resolve as a stale namespace package and raise `ModuleNotFoundError`. If a brand-new subpackage imports under `python -c` but not under pytest, suspect environment contamination, not your code.
The CI `lint.yml``shellcheck` job currently reports and blocks only
error-severity findings. Warnings such as SC2155 are intentionally outside this
job until a follow-up cleanup tightens the threshold.
### Manual testing
#### Testing setup
@@ -149,7 +198,7 @@ the command templates in templates/commands/ to understand what each command
invokes. Use these mapping rules:
- templates/commands/X.md → the command it defines
- scripts/bash/Y.sh or scripts/powershell/Y.ps1 → every command that invokes that script (grep templates/commands/ for the script name). Also check transitive dependencies: if the changed script is sourced by other scripts (e.g., common.sh is sourced by create-new-feature.sh, check-prerequisites.sh, setup-plan.sh, update-agent-context.sh), then every command invoking those downstream scripts is also affected
- scripts/bash/Y.sh or scripts/powershell/Y.ps1 → every command that invokes that script (grep templates/commands/ for the script name). Also check transitive dependencies: if the changed script is sourced by other scripts (e.g., common.sh is sourced by create-new-feature.sh, check-prerequisites.sh, setup-plan.sh), then every command invoking those downstream scripts is also affected
- templates/Z-template.md → every command that consumes that template during execution
- src/specify_cli/*.py → CLI commands (`specify init`, `specify check`, `specify extension *`, `specify preset *`); test the affected CLI command and, for init/scaffolding changes, at minimum test /speckit.specify
- extensions/X/commands/* → the extension command it defines
<h3><em>Define what to build before building it — with any AI coding agent.</em></h3>
</div>
<p align="center">
<strong>An open source toolkit that allows you to focus on product scenarios and predictable outcomes instead of vibe coding every piece from scratch.</strong>
<strong>An open source toolkit for building high-quality software with any AI coding agent — a ready-to-use spec-driven process (or bring your own), endlessly extensible, community-driven, and built for your whole organization.</strong>
</p>
<p align="center">
@@ -26,12 +26,12 @@
- [🤖 Supported AI Coding Agent Integrations](#-supported-ai-coding-agent-integrations)
@@ -44,12 +44,18 @@ 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`):
To check for updates or upgrade the installed CLI, use the self-management commands. See the [Upgrade Guide](./docs/upgrade.md) for detailed scenarios and customization options.
```bash
# Check whether a newer release is available (read-only — does not modify anything)
specify self check
# Preview what would run, without actually upgrading
specify self upgrade --dry-run
# Upgrade in place to the latest stable release (auto-detects uv tool vs pipx install)
specify self upgrade
# Or pin a specific release tag (replace vX.Y.Z[suffix] with your desired release tag)
specify self upgrade --tag vX.Y.Z[suffix]
```
Bare `specify self upgrade` executes immediately, matching the no-prompt behavior of commands like `pip install -U` and `npm update`. For `uv tool` installs, it runs `uv tool install specify-cli --force --from <git ref>` under the hood so pinned release tags work, including dev, alpha/beta/rc, or build metadata suffixes. `uvx` (ephemeral) runs and source checkouts are detected and produce path-specific guidance instead of running an installer. Set `SPECIFY_UPGRADE_TIMEOUT_SECS` to cap how long the installer subprocess may run (default: no timeout — interrupt with `Ctrl+C` if needed).
### 3. Establish project principles
Launch your coding agent in the project directory. Most agents expose spec-kit as `/speckit.*` slash commands; Codex CLI in skills mode uses `$speckit-*` instead.
Launch your coding agent in the project directory. Most agents expose spec-kit as `/speckit.*` slash commands; Codex CLI in skills mode uses `$speckit-*` instead; GitHub Copilot CLI uses `/agents` to select the agent or address it directly in a prompt.
Use the **`/speckit.constitution`** command to create your project's governing principles and development guidelines that will guide all subsequent development.
@@ -115,13 +139,14 @@ Explore community-contributed resources on the [Spec Kit docs site](https://gith
- [Extensions](https://github.github.io/spec-kit/community/extensions.html) — commands, hooks, and capabilities
- [Presets](https://github.github.io/spec-kit/community/presets.html) — template and terminology overrides
- [Bundles](https://github.github.io/spec-kit/community/bundles.html) — role and team stacks composed from existing components
- [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
@@ -133,7 +158,7 @@ Run `specify integration list` to see all available integrations in your install
After running `specify init`, your AI coding agent will have access to these slash commands for structured development. For integrations that support skills mode, passing `--integration <agent> --integration-options="--skills"` installs agent skills instead of slash-command prompt files.
#### Core Commands
### Core Commands
Essential commands for the Spec-Driven Development workflow:
@@ -145,8 +170,9 @@ Essential commands for the Spec-Driven Development workflow:
| `/speckit.taskstoissues` | `speckit-taskstoissues`| Convert generated task lists into GitHub issues for tracking and execution |
| `/speckit.implement` | `speckit-implement` | Execute all tasks to build the feature according to the plan |
| `/speckit.converge` | `speckit-converge` | Assess the codebase against spec/plan/tasks and append remaining work as new tasks |
#### Optional Commands
### Optional Commands
Additional commands for enhanced quality and validation:
@@ -209,6 +235,58 @@ For example, presets could restructure spec templates to require regulatory trac
See the [Presets reference](https://github.github.io/spec-kit/reference/presets.html) for the full command guide, including resolution order and priority stacking.
## 📦 Bundles: Role-Based Setups
Extensions and presets are individual building blocks. A **bundle** packages a
curated set of them — extensions, presets, steps, and workflows — into a single,
versioned, role-oriented setup so a whole team persona (product manager, business
analyst, security researcher, developer, …) can be provisioned with one command.
A bundle is described by a hand-written `bundle.yml` manifest. It pins each
component to a version and, optionally, targets a specific integration; a bundle
with no `integration` is **agnostic** and inherits whatever integration the
project already uses.
```bash
# Discover bundles in the active catalog stack
specify bundle search [<query>]
# Inspect the exact component set a bundle will add (equals what install does)
specify bundle info <bundle-id>
# Install a bundle's full component set in one operation
specify bundle install <bundle-id>
# See what's installed, then update or remove non-destructively
specify bundle list
specify bundle update <bundle-id> # or --all
specify bundle remove <bundle-id> # removes only this bundle's components
```
Bundles resolve from a **priority-ordered catalog stack** (project > user >
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. Distribution is hosting the built
artifact and adding a catalog source; community bundle submissions use the
<summary>Click to expand the detailed step-by-step walkthrough</summary>
You can use the Specify CLI to bootstrap your project, which will bring in the required artifacts in your environment. Run:
```bash
specify init <project_name>
```
Or initialize in the current directory:
```bash
specify init .
# or use the --here flag
specify init --here
# Skip confirmation when the directory already has files
specify init . --force
# or
specify init --here --force
```

In an interactive terminal, you will be prompted to select the coding agent integration you are using. In non-interactive sessions, such as CI or piped runs, `specify init` defaults to GitHub Copilot unless you pass `--integration`. You can also proactively specify the integration directly in the terminal:
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, or Mistral Vibe 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:
Go to the project folder and run your coding agent. In our example, we're using `claude`.

You will know that things are configured correctly if you see the `/speckit.constitution`, `/speckit.specify`, `/speckit.plan`, `/speckit.tasks`, and `/speckit.implement` commands available.
The first step should be establishing your project's governing principles using the `/speckit.constitution` command. This helps ensure consistent decision-making throughout all subsequent development phases:
```text
/speckit.constitution Create principles focused on code quality, testing standards, user experience consistency, and performance requirements. Include governance for how these principles should guide technical decisions and implementation choices.
```
This step creates or updates the `.specify/memory/constitution.md` file with your project's foundational guidelines that the coding agent will reference during specification, planning, and implementation phases.
### **STEP 2:** Create project specifications
With your project principles established, you can now create the functional specifications. Use the `/speckit.specify` command and then provide the concrete requirements for the project you want to develop.
> [!IMPORTANT]
> Be as explicit as possible about *what* you are trying to build and *why*. **Do not focus on the tech stack at this point**.
An example prompt:
```text
Develop Taskify, a team productivity platform. It should allow users to create projects, add team members,
assign tasks, comment and move tasks between boards in Kanban style. In this initial phase for this feature,
let's call it "Create Taskify," let's have multiple users but the users will be declared ahead of time, predefined.
I want five users in two different categories, one product manager and four engineers. Let's create three
different sample projects. Let's have the standard Kanban columns for the status of each task, such as "To Do,"
"In Progress," "In Review," and "Done." There will be no login for this application as this is just the very
first testing thing to ensure that our basic features are set up. For each task in the UI for a task card,
you should be able to change the current status of the task between the different columns in the Kanban work board.
You should be able to leave an unlimited number of comments for a particular card. You should be able to, from that task
card, assign one of the valid users. 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.
```
After this prompt is entered, you should see Claude Code kick off the planning and spec drafting process. Claude Code will also trigger some of the built-in scripts to set up the repository.
Once this step is completed, you should have a new branch created (e.g., `001-create-taskify`), as well as a new specification in the `specs/001-create-taskify` directory.
The produced specification should contain a set of user stories and functional requirements, as defined in the template.
At this stage, your project folder contents should resemble the following:
```text
.
├── .specify
│ ├── memory
│ │ └── constitution.md
│ ├── scripts
│ │ └── bash
│ │ ├── check-prerequisites.sh
│ │ ├── common.sh
│ │ ├── create-new-feature.sh
│ │ ├── setup-plan.sh
│ │ └── setup-tasks.sh
│ └── templates
│ ├── plan-template.md
│ ├── spec-template.md
│ └── tasks-template.md
└── specs
└── 001-create-taskify
└── spec.md
```
### **STEP 3:** Functional specification clarification (required before planning)
With the baseline specification created, you can go ahead and clarify any of the requirements that were not captured properly within the first shot attempt.
You should run the structured clarification workflow **before** creating a technical plan to reduce rework downstream.
Preferred order:
1. Use `/speckit.clarify` (structured) – sequential, coverage-based questioning that records answers in a Clarifications section.
2. Optionally follow up with ad-hoc free-form refinement if something still feels vague.
If you intentionally want to skip clarification (e.g., spike or exploratory prototype), explicitly state that so the agent doesn't block on missing clarifications.
Example free-form refinement prompt (after `/speckit.clarify` if still needed):
```text
For each sample project or project that you create there should be a variable number of tasks between 5 and 15
tasks for each one randomly distributed into different states of completion. Make sure that there's at least
one task in each stage of completion.
```
You should also ask Claude Code to validate the **Review & Acceptance Checklist**, checking off the things that are validated/pass the requirements, and leave the ones that are not unchecked. The following prompt can be used:
```text
Read the review and acceptance checklist, and check off each item in the checklist if the feature spec meets the criteria. Leave it empty if it does not.
```
It's important to use the interaction with Claude Code as an opportunity to clarify and ask questions around the specification - **do not treat its first attempt as final**.
### **STEP 4:** Generate a plan
You can now be specific about the tech stack and other technical requirements. You can use the `/speckit.plan` command that is built into the project template with a prompt like this:
```text
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.
```
The output of this step will include a number of implementation detail documents, with your directory tree resembling this:
```text
.
├── CLAUDE.md
├── .specify
│ ├── memory
│ │ └── constitution.md
│ ├── scripts
│ │ └── bash
│ │ ├── check-prerequisites.sh
│ │ ├── common.sh
│ │ ├── create-new-feature.sh
│ │ ├── setup-plan.sh
│ │ └── setup-tasks.sh
│ └── templates
│ ├── CLAUDE-template.md
│ ├── plan-template.md
│ ├── spec-template.md
│ └── tasks-template.md
└── specs
└── 001-create-taskify
├── contracts
│ ├── api-spec.json
│ └── signalr-spec.md
├── data-model.md
├── plan.md
├── quickstart.md
├── research.md
└── spec.md
```
Check the `research.md` document to ensure that the right tech stack is used, based on your instructions. You can ask Claude Code to refine it if any of the components stand out, or even have it check the locally-installed version of the platform/framework you want to use (e.g., .NET).
Additionally, you might want to ask Claude Code to research details about the chosen tech stack if it's something that is rapidly changing (e.g., .NET Aspire, JS frameworks), with a prompt like this:
```text
I want you to go through the implementation plan and implementation details, looking for areas that could
benefit from additional research as .NET Aspire is a rapidly changing library. For those areas that you identify that
require further research, I want you to update the research document with additional details about the specific
versions that we are going to be using in this Taskify application and spawn parallel research tasks to clarify
any details using research from the web.
```
During this process, you might find that Claude Code gets stuck researching the wrong thing - you can help nudge it in the right direction with a prompt like this:
```text
I think we need to break this down into a series of steps. First, identify a list of tasks
that you would need to do during implementation that you're not sure of or would benefit
from further research. Write down a list of those tasks. And then for each one of these tasks,
I want you to spin up a separate research task so that the net results is we are researching
all of those very specific tasks in parallel. What I saw you doing was it looks like you were
researching .NET Aspire in general and I don't think that's gonna do much for us in this case.
That's way too untargeted research. The research needs to help you solve a specific targeted question.
```
> [!NOTE]
> Claude Code might be over-eager and add components that you did not ask for. Ask it to clarify the rationale and the source of the change.
### **STEP 5:** Have Claude Code validate the plan
With the plan in place, you should have Claude Code run through it to make sure that there are no missing pieces. You can use a prompt like this:
```text
Now I want you to go and audit the implementation plan and the implementation detail files.
Read through it with an eye on determining whether or not there is a sequence of tasks that you need
to be doing that are obvious from reading this. Because I don't know if there's enough here. For example,
when I look at the core implementation, it would be useful to reference the appropriate places in the implementation
details where it can find the information as it walks through each step in the core implementation or in the refinement.
```
This helps refine the implementation plan and helps you avoid potential blind spots that Claude Code missed in its planning cycle. Once the initial refinement pass is complete, ask Claude Code to go through the checklist once more before you can get to the implementation.
You can also ask Claude Code (if you have the [GitHub CLI](https://docs.github.com/en/github-cli/github-cli) installed) to go ahead and create a pull request from your current branch to `main` with a detailed description, to make sure that the effort is properly tracked.
> [!NOTE]
> Before you have the agent implement it, it's also worth prompting Claude Code to cross-check the details to see if there are any over-engineered pieces (remember - it can be over-eager). If over-engineered components or decisions exist, you can ask Claude Code to resolve them. Ensure that Claude Code follows the constitution in `.specify/memory/constitution.md` as the foundational piece that it must adhere to when establishing the plan.
### **STEP 6:** Generate task breakdown with /speckit.tasks
With the implementation plan validated, you can now break down the plan into specific, actionable tasks that can be executed in the correct order. Use the `/speckit.tasks` command to automatically generate a detailed task breakdown from your implementation plan:
```text
/speckit.tasks
```
This step creates a `tasks.md` file in your feature specification directory that contains:
- **Task breakdown organized by user story** - Each user story becomes a separate implementation phase with its own set of tasks
- **Dependency management** - Tasks are ordered to respect dependencies between components (e.g., models before services, services before endpoints)
- **Parallel execution markers** - Tasks that can run in parallel are marked with `[P]` to optimize development workflow
- **File path specifications** - Each task includes the exact file paths where implementation should occur
- **Test-driven development structure** - If tests are requested, test tasks are included and ordered to be written before implementation
- **Checkpoint validation** - Each user story phase includes checkpoints to validate independent functionality
The generated tasks.md provides a clear roadmap for the `/speckit.implement` command, ensuring systematic implementation that maintains code quality and allows for incremental delivery of user stories.
### **STEP 7:** Implementation
Once ready, use the `/speckit.implement` command to execute your implementation plan:
```text
/speckit.implement
```
The `/speckit.implement` command will:
- Validate that all prerequisites are in place (constitution, spec, plan, and tasks)
- Parse the task breakdown from `tasks.md`
- Execute tasks in the correct order, respecting dependencies and parallel execution markers
- Follow the TDD approach defined in your task plan
- Provide progress updates and handle errors appropriately
> [!IMPORTANT]
> The coding agent will execute local CLI commands (such as `dotnet`, `npm`, etc.) - make sure you have the required tools installed on your machine.
Once the implementation is complete, test the application and resolve any runtime errors that may not be visible in CLI logs (e.g., browser console errors). You can copy and paste such errors back to your coding agent for resolution.
> 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 are published in [`bundles/catalog.community.json`](https://github.com/github/spec-kit/blob/main/bundles/catalog.community.json) and listed below. The built-in community source is discovery-only: `specify bundle search` and `specify bundle info` can inspect entries, but installing by ID requires explicitly adding an install-allowed catalog. Explicit catalogs use a higher default precedence than the built-in community source. To submit a bundle for review, file a [Bundle Submission](https://github.com/github/spec-kit/issues/new?template=bundle_submission.yml) issue.
| Bundle | Purpose | Role or team | Provides | Required catalogs | URL |
- 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.
The following community-contributed extensions are available in [`catalog.community.json`](https://github.com/github/spec-kit/blob/main/extensions/catalog.community.json):
**Categories:**
**Categories** (common values, but any string is allowed):
-`docs` — reads, validates, or generates spec artifacts
-`code` — reviews, validates, or modifies source code
@@ -15,48 +15,65 @@ The following community-contributed extensions are available in [`catalog.commun
-`integration` — syncs with external platforms
-`visibility` — reports on project health or progress
-`Read-only` — produces reports without modifying files
-`Read+Write` — modifies files, creates artifacts, or updates specs
-`read-only` — produces reports without modifying files (displayed as `Read-only` in the table)
-`read-write` — modifies files, creates artifacts, or updates specs (displayed as `Read+Write` in the table)
> [!TIP]
> Extension authors can declare `category` and `effect` in their `extension.yml` under the `extension:` block. These fields are also available in `catalog.community.json` for tooling and the CLI (`specify extension info`).
| Extension | Purpose | Category | Effect | URL |
|-----------|---------|----------|--------|-----|
| Agent Assign | Assign specialized Claude Code agents to spec-kit tasks for targeted execution | `process` | Read+Write | [spec-kit-agent-assign](https://github.com/xymelon/spec-kit-agent-assign) |
| 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 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) |
| Blueprint Index — Living Architecture Map | A living architecture map for spec-driven projects, kept honest by a deterministic, low-friction, machine-first CI gate (JSON, self-healable) that blocks only when the map contradicts the specs or code. Brownfield or greenfield. | `process` | Read+Write | [spec-kit-blueprint](https://github.com/ogil109/spec-kit-blueprint) |
| Branch Convention | Configurable branch and folder naming conventions for /specify with presets and custom patterns | `process` | Read+Write | [spec-kit-branch-convention](https://github.com/Quratulain-bilal/spec-kit-branch-convention) |
| 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) |
| Coding Standards Drift Control | Generate coding-standards drift reports and remediation tasks for active Spec Kit features | `code` | Read+Write | [spec-kit-coding-standards-drift-control](https://github.com/benizzio/spec-kit-coding-standards-drift-control) |
| 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) |
| DocGuard — CDD Enforcement | Canonical-Driven Development enforcement. Validates, scores, and traces project documentation with automated checks, AI-driven workflows, and spec-kit hooks. Zero NPM runtime dependencies. | `docs` | Read+Write | [spec-kit-docguard](https://github.com/raccioly/docguard) |
| 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 | The only doc-integrity engine with an MCP server, SARIF/JUnit output, and a deterministic zero-LLM core. Validates, scores, and traces documentation against code — 27 validators, stable finding codes, adoption baseline for legacy repos, compliance-evidence reports, GitHub Action with PR annotations, spec-kit hooks. Pure Node.js, one pinned dep. | `docs` | Read+Write | [spec-kit-docguard](https://github.com/raccioly/docguard) |
| Dotdog | Import GitHub Spec Kit artifacts into local knowledge graphs for validation, analysis, search, and MCP queries. | `docs` | Read+Write | [dotdog](https://github.com/specdog/dotdog) |
| 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) |
| Figma Starter | Turns a Figma section's screens into per-screen spec.md files, an app-level user-stories.md, and a build-order.md, then hands off to /speckit.specify | `integration` | Read+Write | [spec-kit-figma-starter](https://github.com/wavemaker/spec-kit-figma-starter) |
| 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) |
| Interactive HTML Preview | Generate self-contained interactive HTML prototypes from Spec Kit artifacts | `docs` | Read+Write | [spec-kit-preview](https://github.com/bigsmartben/spec-kit-preview) |
| 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, 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](https://github.com/ashbrener/spec-kit-linear) |
| 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) |
| Linear Weave | Weave Spec Kit into Linear: pull requirements, mirror tasks.md into sub-issues, sync statuses | `integration` | Read+Write | [spec-kit-linear-weave](https://github.com/tonydwoodhouse/spec-kit-linear-weave) |
| 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) |
| MAQA CI/CD Gate | Auto-detects GitHub Actions, CircleCI, GitLab CI, and Bitbucket Pipelines. Blocks QA handoff until pipeline is green. | `process` | Read+Write | [spec-kit-maqa-ci](https://github.com/GenieRobot/spec-kit-maqa-ci) |
@@ -68,32 +85,39 @@ The following community-contributed extensions are available in [`catalog.commun
| MDE | Minimal model-driven engineering workflow with setup, next, and status commands | `process` | Read+Write | [spec-kit-mde](https://github.com/AI-MDE/spec-kit-mde) |
| Memory Loader | Loads .specify/memory/ files before lifecycle commands so LLM agents have project governance context | `docs` | Read-only | [spec-kit-memory-loader](https://github.com/KevinBrown5280/spec-kit-memory-loader) |
| Memory MD | Spec Kit extension for repository-native Markdown memory that captures durable decisions, bugs, and project context | `docs` | Read+Write | [spec-kit-memory-hub](https://github.com/DyanGalih/spec-kit-memory-hub) |
| MemoryLint | Agent memory governance tool: Automatically audits and fixes boundary conflicts between AGENTS.md and the constitution. | `process` | Read+Write | [memorylint](https://github.com/RbBtSn0w/spec-kit-extensions/tree/main/memorylint) |
| 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) |
| OKF Knowledge Bundle Generator | Generates and maintains an Open Knowledge Format (OKF v0.1) knowledge bundle from a source-code repository, mining git history for significance and rationale, and resolving open questions with the user | `docs` | Read+Write | [speckit_ofk](https://github.com/alexcpn/speckit_ofk) |
| 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) |
| PatchWarden Evidence Pack | Map Spec Kit tasks into a guarded PatchWarden Goal and export bounded, traceable evidence for an accepted lineage. | `process` | Read+Write | [spec-kit-patchwarden](https://github.com/jiezeng2004-design/spec-kit-patchwarden) |
| 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) |
| 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 across agent hooks, git checks, and CI pipelines with one policy file and one verify entrypoint for identical results at every boundary. | `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) |
| Spec Validate | Comprehension validation, review gating, and approval state for spec-kit artifacts — staged quizzes, peer review SLA, and a hard gate before /speckit.implement | `process` | Read+Write | [spec-kit-spec-validate](https://github.com/aeltayeb/spec-kit-spec-validate) |
| Spec-Kit BDD | ATDD/BDD extension: convert specs to Gherkin scenarios, scaffold step definitions, and verify acceptance test coverage | `process` | Read+Write | [spec-kit-bdd](https://github.com/RSginer/spec-kit-bdd) |
| Spec2Cloud | Spec-driven workflow tuned for shipping to Azure | `process` | Read+Write | [spec2cloud](https://github.com/Azure-Samples/Spec2Cloud) |
| SpecKit Companion | Live spec-driven progress — lifecycle capture, status, resume, and a turbo pipeline profile | `visibility` | Read+Write | [speckit-companion](https://github.com/alfredoperez/speckit-companion) |
| SpecTest | Auto-generate test scaffolds from spec criteria, map coverage, and find untested requirements | `code` | Read+Write | [spec-kit-spectest](https://github.com/Quratulain-bilal/spec-kit-spectest) |
| Squad Bridge | Bootstrap and synchronize a Squad agent team from your Speckit spec and tasks. | `process` | Read+Write | [spec-kit-squad](https://github.com/jwill824/spec-kit-squad) |
| Staff Review Extension | Staff-engineer-level code review that validates implementation against spec, checks security, performance, and test coverage | `code` | Read-only | [spec-kit-staff-review](https://github.com/arunt14/spec-kit-staff-review) |
| Status Report | Project status, feature progress, and next-action recommendations for spec-driven workflows | `visibility` | Read-only | [Open-Agent-Tools/spec-kit-status](https://github.com/Open-Agent-Tools/spec-kit-status) |
| Superpowers Bridge | Orchestrates obra/superpowers skills within the spec-kit SDD workflow across the full lifecycle (clarification, TDD, review, verification, critique, debugging, branch completion) | `process` | Read+Write | [superpowers-bridge](https://github.com/RbBtSn0w/spec-kit-extensions/tree/main/superpowers-bridge) |
| Superpowers Bridge (WangX0111) | Bridges spec-kit with obra/superpowers (brainstorming, TDD, subagent, code-review) into a unified, resumable workflow with graceful degradation and session progress tracking | `process` | Read+Write | [superspec](https://github.com/WangX0111/superspec) |
| Superpowers Bridge | Bridges selected Superpowers disciplines into Spec Kit as evidence-first trust gates for agent workflows. | `process` | Read+Write | [superpowers-bridge](https://github.com/RbBtSn0w/spec-kit-extensions/tree/main/superpowers-bridge) |
| Superspec | Bridges spec-kit with obra/superpowers (brainstorming, TDD, subagent, code-review) into a unified, resumable workflow with graceful degradation and session progress tracking | `process` | Read+Write | [superspec](https://github.com/WangX0111/superspec) |
| Tasks to GitHub Project | Publish and synchronize Spec Kit tasks as cards on a GitHub Project (v2) kanban board, with priority and status sync between spec.md/tasks.md and the board. | `integration` | Read+Write | [spec-kit-tasks-to-project](https://github.com/mancioshell/spec-kit-tasks-to-project) |
| Team Assign | Assign tasks.md items to human engineers, split into subtasks, and generate a per-engineer workboard | `process` | Read+Write | [spec-kit-team-assign](https://github.com/tarunkumarbhati/spec-kit-team-assign) |
| Test Coverage Drift Control | Generate incremental coverage drift reports and planned remediation tasks after implementation | `code` | Read+Write | [spec-kit-test-coverage-drift-control](https://github.com/benizzio/spec-kit-test-coverage-drift-control) |
| Time Machine | Retroactively apply the full SDD workflow to existing codebases — analyse, spec, and ship feature-by-feature | `process` | Read+Write | [spec-kit-time-machine](https://github.com/teeyo/spec-kit-time-machine) |
| TinySpec | Lightweight single-file workflow for small tasks — skip the heavy multi-step SDD process | `process` | Read+Write | [spec-kit-tinyspec](https://github.com/Quratulain-bilal/spec-kit-tinyspec) |
| 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 Review Ship | Adds verify and review quality gates plus transactional merge, cleanup, and delivery summary | `process` | Read+Write | [spec-kit-verify-review-ship](https://github.com/cadugevaerd/spec-kit-verify-review-ship) |
| 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) |
> Community projects listed here are independently created and maintained by their respective authors. They are **not reviewed, nor endorsed, nor supported by GitHub**. Review their source code before installation and use at your own discretion.
> Community projects listed here are independently created and maintained by their respective authors. Unless explicitly marked as a **first-party GitHub project**, they are **not reviewed, nor endorsed, nor supported by GitHub**. Review their source code before installation and use at your own discretion.
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.
- **[spec-kit-copilot](https://github.com/github/spec-kit-copilot)** — _First-party GitHub project._ A GitHub Copilot **skills plugin** that exposes the Spec Kit `specify` CLI to the Copilot agent in both the Copilot CLI and the GitHub Copilot app. It provides a focused skill per `specify` command group — setup, init, check, extensions, presets, bundles, workflows, workflow steps, and self-upgrade — so you can navigate and drive the entire Spec Kit ecosystem through natural language, letting Copilot decide when and how to run the right `specify` commands on your behalf.
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
Extensions add new capabilities to Spec Kit — domain-specific commands, external tool integrations, quality gates, and more. Over 90 community extensions are available from 50+ authors, covering everything from accessibility governance to multi-agent orchestration.
Extensions add new capabilities to Spec Kit — domain-specific commands, external tool integrations, quality gates, and more. Over 130 community extensions are available from 70+ authors, covering everything from accessibility governance to multi-agent orchestration.
[Browse community extensions →](extensions.md)
@@ -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.
| Autonomous Run Governance | Adds permission-bounded, evidence-first governance for complete autonomous Spec Kit delivery, including validated status, stop, explicit resume, exact-head proof, post-merge closeout, retrospective learning, and an optional policy-driven intake-review gate before feature creation. | 13 templates, 5 commands, 4 scripts | — | [spec-kit-preset-autonomous-run-governance](https://github.com/hindermath/spec-kit-preset-autonomous-run-governance) |
| 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) |
| Cross-Platform Governance | Adds Bash/PowerShell and read-only check parity, root-path and native-override review, Unix man pages, bilingual help, Verb-Noun discipline, and audit-ready evidence. | 8 templates, 3 commands | — | [spec-kit-preset-cross-platform-governance](https://github.com/hindermath/spec-kit-preset-cross-platform-governance) |
| Explicit Task Dependencies | Adds explicit `(depends on T###)` dependency declarations and an Execution Wave DAG to tasks.md for parallel scheduling | 1 template, 1 command | — | [spec-kit-preset-explicit-task-dependencies](https://github.com/Quratulain-bilal/spec-kit-preset-explicit-task-dependencies) |
| Fiction Book Writing | It adapts the Spec-Driven Development workflow for storytelling to create books or audiobooks (with annotations) in 12 languages: features become story elements, specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports single and multi-POV, all major plot structure frameworks, and two style modes: an author voice sample or humanized AI prose principles. Supports interactive elements like brainstorming, interview, roleplay and extras like statistics, cover builder and bio command. Export with templates for KDP, D2D etc. | 25 templates, 33 commands, 2 scripts | — | [speckit-preset-fiction-book-writing](https://github.com/adaumann/speckit-preset-fiction-book-writing) |
| Game Narrative Writing | Spec-Driven Development for interactive game narrative pre-production for video games. Authors write in a portable generic format, Twine/Sugarcube (.twee) or Ink (.ink). Covers choice-IF, visual novels, and branching dialogue. Supports Tier 1 mechanic hooks (flag, counter, inventory, timer, trust, currency, npc_state, ending_condition), multi-ending design, series carry-over variable registry, and NPC-focused character architecture. | 22 templates, 36 commands, 2 scripts | — | [speckit-preset-game-narrative-writing](https://github.com/adaumann/speckit-preset-game-narrative-writing) |
| iSAQB Architecture Governance | Adds general iSAQB/CPSA-F and arc42 architecture governance: goals, context, building blocks, runtime and deployment views, quality scenarios, ADRs, risks, and technical debt | 13 templates, 3 commands | — | [spec-kit-preset-isaqb-architecture-governance](https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance) |
| Fiction Book Writing | It adapts the Spec-Driven Development workflow for storytelling to create books or audiobooks (with annotations) in 12 languages: features become story elements, specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports single and multi-POV, all major plot structure frameworks, and two style modes: an author voice sample or humanized AI prose principles. Supports interactive elements like brainstorming, interview, roleplay, and extras like statistics, cover builder, illustration builder, and bio command. Export with templates for KDP, D2D, etc. | 26 templates, 34 commands, 2 scripts | — | [speckit-preset-fiction-book-writing](https://github.com/adaumann/speckit-preset-fiction-book-writing) |
| Game Narrative Writing | Preset for game narrative design and interactive storytelling. It adapts the Spec-Driven Development workflow for game narratives: features become story mechanics, specs become narrative briefs, plans become story maps, and tasks become dialogue and scene-writing tasks. Supports branching narratives, player agency systems, state machines, and interactive dialogue trees. | 37 templates, 34 commands, 5 scripts | — | [speckit-preset-game-narrative-writing](https://github.com/adaumann/speckit-preset-game-narrative-writing) |
| Intake Authoring Governance | Governs traceable intake CRUD, bounded public HTTPS sources, and explicitly approved single or series authoring without granting execution authority. | 10 templates, 5 commands, 4 scripts | — | [spec-kit-preset-intake-authoring-governance](https://github.com/hindermath/spec-kit-preset-intake-authoring-governance) |
| Intake Review Governance | Reviews single, series, and campaign intake files before Spec Kit execution and binds accepted outcomes to normalized content hashes. | 8 templates, 3 commands, 2 scripts | — | [spec-kit-preset-intake-review-governance](https://github.com/hindermath/spec-kit-preset-intake-review-governance) |
| iSAQB Architecture Governance | Adds iSAQB/CPSA-F and arc42 architecture governance with audit-ready evidence for goals, views, resumability, partial-failure scenarios, ADRs, risks, and technical debt. | 13 templates, 3 commands | — | [spec-kit-preset-isaqb-architecture-governance](https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance) |
| Jira Issue Tracking | Overrides `speckit.taskstoissues` to create Jira epics, stories, and tasks instead of GitHub Issues via Atlassian MCP tools | 1 command | — | [spec-kit-preset-jira](https://github.com/luno/spec-kit-preset-jira) |
| Model Driven Engineering | Focuses on streamlined commands, app repository support, cross-spec support, and capability-aware project memory for model-driven engineering workflows | 6 templates, 11 commands | MDE extension | [spec-kit-preset-mde](https://github.com/AI-MDE/spec-kit-preset-mde) |
| Multi-Repo Branching | Coordinates feature branch creation across multiple git repositories (independent repos and submodules) during plan and tasks phases | 2 commands | — | [spec-kit-preset-multi-repo-branching](https://github.com/sakitA/spec-kit-preset-multi-repo-branching) |
| Parallel Autonomous Run Governance | Coordinates isolated autonomous Spec Kit campaigns with bounded concurrency, mixed agents, resumable consolidation, governed post-merge closeout, schema 1.2, and an optional current intake-review gate before worker scheduling. | 9 templates, 5 commands, 2 scripts | autonomous-run-governance >=0.3.2; optional: intake-review-governance >=0.1.0 | [spec-kit-preset-parallel-autonomous-run-governance](https://github.com/hindermath/spec-kit-preset-parallel-autonomous-run-governance) |
| 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) |
| 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) |
Use flow-forward when each feature directory should remain a historical record.
When you add another feature or make a substantial follow-up change, create a
new feature spec through your installed `/speckit.specify` command and continue
through the standard flow:
1. Run `/speckit.specify` to create a new feature directory under `specs/`.
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
cross-links when a new directory supersedes or extends earlier work.
## Living Spec
Use living spec when `spec.md` is the contract and `plan.md` and `tasks.md` are
derived from it.
When intended behavior changes, revise the existing `spec.md` first. Then
regenerate or manually revise downstream artifacts so they match the updated
spec:
1. Start from a clean working tree or a dedicated branch so every generated
change is reviewable.
2. Update `spec.md` with `/speckit.clarify` or an explicit edit.
3. Rerun `/speckit.plan` or revise `plan.md` so the technical approach matches
the revised spec.
4. Rerun `/speckit.tasks` or revise `tasks.md` so implementation work matches
the revised plan.
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
explicitly.
## Flow-Back Spec
Use flow-back when implementation discoveries are allowed to reshape the
artifact set.
In this model, the first useful edit can happen wherever the insight lands:
`spec.md`, `plan.md`, `tasks.md`, or the implementation. After the change, bring
the artifact set back into alignment:
1. Capture the discovery in the artifact closest to the work.
2. Decide whether it changes intended behavior, implementation strategy, task
breakdown, or only code.
3. Update any other artifacts that now disagree with the accepted direction.
4. Run `/speckit.analyze` to check for gaps across `spec.md`, `plan.md`, and
`tasks.md`.
5. Continue implementation only after the artifact set describes the behavior
and approach you want future contributors to trust.
Flow-back is flexible, but it requires discipline. Do not leave a lower-level
change in `tasks.md` or code if `spec.md` still says something different and the
spec is meant to remain trustworthy.
## Before Updating Spec Kit Project Files
Before refreshing Spec Kit project files with the terminal command
`specify init --here --force --integration <your-agent>`, protect any
project-specific material that lives outside `specs/`, especially
`.specify/memory/constitution.md` and customized files under
`.specify/templates/` or `.specify/scripts/`. Use `<your-agent>` for the AI
coding agent integration used by the target project.
Your `specs/` directory is not part of the template package, but shared project
**Define what to build before building it — with any AI coding agent.**
**Spec-Driven Development or your own process — step by step or as an automated workflow.**
Spec Kit is a toolkit for [Spec-Driven Development](concepts/sdd.md) (SDD), a methodology that puts specifications at the center of AI-assisted software development. Instead of jumping straight to code, you describe *what* to build, refine it through structured phases, and let your AI coding agent implement it.
Spec Kit is an extensible, intent-driven harness that pushes any coding agent beyond code, guiding it across your SDLC or any business process. Use it for [Spec-Driven Development](concepts/sdd.md) (SDD), where you describe _what_ to build and refine it through structured phases. Run it step by step, automate it end to end, or shape a process of your own, keeping intent at the center.
@@ -31,9 +31,9 @@ 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, Claude, Forge, Kiro, and more. Switch freely between agents with a single command. No lock-in.
<span class="pillar-stat">35 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.
Run `specify init` with your agent of choice and Spec Kit sets up the right command files and directory structures automatically. If your agent isn't listed, the `generic` integration is an escape hatch for any tool.
<a href="reference/integrations.md" class="pillar-link">See all integrations →</a>
@@ -43,17 +43,21 @@ Run `specify init` with your agent of choice and Spec Kit sets up the right comm
### Make it your own
<span class="pillar-stat">105 community extensions</span> (60+ authors), <span class="pillar-stat">22 presets</span>, and growing. Tune the core process with presets, extend it with extensions, orchestrate it with workflows, or replace it entirely. Build and publish your own.
<span class="pillar-stat">138 community extensions</span> (70+ authors), <span class="pillar-stat">25 presets</span>, and growing. Tune the core process with presets, extend it with extensions, orchestrate it with workflows, and package it all up as bundles you can share — or replace the process entirely. The process itself lives in these building blocks, so you're never locked to SDD, or even to software.
@@ -61,12 +65,12 @@ Including entirely different SDD processes:
### Integrate into your organization
Works offline, behind firewalls, and on **Windows, macOS, and Linux**. Host your own extension and preset catalogs so your organization controls what gets installed.
Works offline, behind firewalls, and on **Windows, macOS, and Linux**. Host your own catalogs to curate what integrations, extensions, presets, workflows, and bundles your organization discovers and recommends.
Community extensions like CI Guard and Architecture Guard add compliance gates and governance that fit the way your team already works.
@@ -78,31 +82,31 @@ Community extensions like CI Guard and Architecture Guard add compliance gates a
## Built by the community
**200+ contributors** power the Spec Kit ecosystem — from core integrations to entirely new development processes. Anyone can create and publish an extension, preset, or workflow.
**240+ contributors** power the Spec Kit ecosystem — from core integrations to entirely new processes. Anyone can create and publish an extension, preset, or workflow.
<div class="stats-grid">
<div class="stat-item">
<span class="stat-number">106K+</span>
<span class="stat-number">121K+</span>
<span class="stat-label">GitHub stars</span>
</div>
<div class="stat-item">
<span class="stat-number">200+</span>
<span class="stat-number">240+</span>
<span class="stat-label">Contributors</span>
</div>
<div class="stat-item">
<span class="stat-number">30</span>
<span class="stat-number">35</span>
<span class="stat-label">Integrations</span>
</div>
<div class="stat-item">
<span class="stat-number">105</span>
<span class="stat-number">138</span>
<span class="stat-label">Extensions</span>
</div>
<div class="stat-item">
<span class="stat-number">22</span>
<span class="stat-number">25</span>
<span class="stat-label">Presets</span>
</div>
<div class="stat-item">
<span class="stat-number">4</span>
<span class="stat-number">6</span>
<span class="stat-label">Friends projects</span>
</div>
</div>
@@ -143,7 +147,7 @@ Community extensions like CI Guard and Architecture Guard add compliance gates a
Spec Kit is published to PyPI as [`specify-cli`](https://pypi.org/project/specify-cli/), maintained by the Spec Kit maintainers. Installing from PyPI is the second supported install route alongside installing from the [GitHub source](../installation.md#install-from-source--persistent-installation-recommended). Use whichever fits your workflow — both provide the same `specify` CLI.
> [!NOTE]
> The PyPI release version tracks the GitHub release tags (for example, PyPI `0.12.11` corresponds to the `v0.12.11` tag). `specify version` is only a local version/runtime sanity check — it reports the installed version but not where the `specify` executable came from, so it cannot distinguish a PyPI install from a Git install. To confirm the install source, inspect the source metadata your package manager records: `pipx list --json` reports the exact install specification for each tool, and for uv/pip installs you can check the package's [PEP 610](https://peps.python.org/pep-0610/) `direct_url.json` inside its `*.dist-info` directory (a Git or URL install records the repository/archive URL there, while a plain PyPI index install does not create that file). Note that `pip show specify-cli` only prints package metadata and will not see uv/pipx-managed environments from the host interpreter.
## Install Specify CLI
Use whichever Python tool you already have:
```bash
# Using uv (recommended)
uv tool install specify-cli
# Or using pipx
pipx install specify-cli
# Or using pip
pip install specify-cli
```
### Install a specific release
Pin an exact version for reproducible installs (check [PyPI](https://pypi.org/project/specify-cli/#history) or [Releases](https://github.com/github/spec-kit/releases) for available versions):
```bash
# Using uv
uv tool install specify-cli==0.12.11
# Or using pipx
pipx install specify-cli==0.12.11
# Or using pip
pip install specify-cli==0.12.11
```
## Verify
```bash
specify version
```
## Initialize a project
```bash
specify init <PROJECT_NAME> --integration copilot
```
## Upgrade
Upgrade by reinstalling the package through the same tool you used for the original install. If you originally pinned a version, note that `uv tool upgrade` preserves that pin; to move to the newest PyPI release, use an unpinned install command so you do not keep the existing version pin:
```bash
# Using uv
uv tool install --force specify-cli
# Or using pipx
pipx install --force specify-cli
# Or using pip
pip install --upgrade specify-cli
```
> [!NOTE]
> `specify self upgrade` currently rebuilds `uv tool` and `pipx` installs from the GitHub source release URL rather than preserving a PyPI-based installation. If you want to stay on the PyPI route, use the package-manager commands above. A plain `pip install specify-cli` is treated as an unmanaged install — upgrade it with `pip install --upgrade specify-cli`. See the [Upgrade Guide](../upgrade.md) for details.
## Uninstall
```bash
# Using uv
uv tool uninstall specify-cli
# Or using pipx
pipx uninstall specify-cli
# Or using pip
pip uninstall specify-cli
```
## Next steps
Head to the [Quick Start](../quickstart.md) to initialize your first project.
- [Git](https://git-scm.com/downloads)_(optional — required only when the git extension is enabled)_
## Installation
> [!IMPORTANT]
> The only official, maintained packages for Spec Kit come from the [github/spec-kit](https://github.com/github/spec-kit) GitHub repository. Any packages with the same name available on PyPI (e.g. `specify-cli` on pypi.org) are **not** affiliated with this project and are not maintained by the Spec Kit maintainers. For normal installs, use the GitHub-based commands shown below. For offline or air-gapped environments, locally built wheels created from this repository are also valid.
> Spec Kit is distributed through two official channels, both published and maintained by the Spec Kit maintainers: the [github/spec-kit](https://github.com/github/spec-kit) GitHub repository (source installs) and the [`specify-cli`](https://pypi.org/project/specify-cli/) package on [PyPI](https://pypi.org/project/specify-cli/). Either route is supported for normal installs — use the commands shown below. After installing, run `specify version` as a local version/runtime sanity check. It confirms that the `specify` command is available and reports its version, but it does not prove whether the executable came from PyPI or GitHub. For offline or air-gapped environments, locally built wheels created from this repository are also valid.
### Persistent Installation (Recommended)
Spec Kit supports two install routes:
Install once and use everywhere. Replace `vX.Y.Z` with a tag from [Releases](https://github.com/github/spec-kit/releases):
1.**Install from source (GitHub)** — the recommended route, pinned to a release tag.
2.**Install from PyPI** — install the published `specify-cli` package with your usual Python tooling.
### Install from Source — Persistent Installation (Recommended)
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).
@@ -30,12 +35,30 @@ Then initialize a project:
specify init <PROJECT_NAME> --integration copilot
```
### Install from PyPI
Spec Kit is also published to PyPI as [`specify-cli`](https://pypi.org/project/specify-cli/), so you can install it with your preferred Python package manager without referencing the Git URL:
```bash
# Using uv (recommended)
uv tool install specify-cli
# Or using pipx
pipx install specify-cli
# Or using pip
pip install specify-cli
```
To install a specific release, pin the version — for example `uv tool install specify-cli==0.12.11`. See the [PyPI installation guide](install/pypi.md) for details, including how to upgrade.
### One-time Usage
Run directly without installing — see the [One-time usage (uvx)](install/one-time.md) guide.
### Alternative Package Managers
- **PyPI** — see the [PyPI installation guide](install/pypi.md)
- **pipx** — see the [pipx installation guide](install/pipx.md)
- **Enterprise / Air-Gapped** — see the [air-gapped installation guide](install/air-gapped.md)
### Specify Script Type (Shell, PowerShell, or Python)
All automation scripts now have both Bash (`.sh`) and PowerShell (`.ps1`) variants.
Automation scripts are available as Bash (`.sh`), PowerShell (`.ps1`), and Python (`.py`) variants.
Auto behavior:
@@ -68,6 +92,7 @@ Force a specific script type:
```bash
specify init <project_name> --script sh
specify init <project_name> --script ps
specify init <project_name> --script py
```
### Ignore Agent Tools Check
@@ -80,24 +105,34 @@ specify init <project_name> --integration claude --ignore-agent-tools
## Verification
After installation, run the following command to confirm the correct version is installed:
After installation, run the following command as a local version/runtime check:
```bash
specify version
```
This helps verify you are running the official Spec Kit build from GitHub, not an unrelated package with the same name.
This confirms that the `specify` command is available and reporting the expected version. It does not prove whether that executable came from PyPI or GitHub.
**Stay current:** Run `specify self check` periodically to learn whether a newer release is available — it is read-only and never modifies your installation. When you are ready to upgrade, follow the [Upgrade Guide](./upgrade.md).
After initialization, you should see the following commands available in your coding agent:
-`/speckit.specify` - Create specifications
-`/speckit.plan` - Generate implementation plans
-`/speckit.plan` - Generate implementation plans
-`/speckit.tasks` - Break down into actionable tasks
This guide shows how to iterate on the `specify` CLI locally without publishing a release or committing to `main` first.
> Scripts now have both Bash (`.sh`) and PowerShell (`.ps1`) variants. The CLI auto-selects based on OS unless you pass `--script sh|ps`.
> Scripts are available as Bash (`.sh`), PowerShell (`.ps1`), and Python (`.py`) variants. Interactive `specify init` prompts you to choose one; non-interactive runs default to a shell variant for your OS. Pass `--script sh|ps|py` to select explicitly.
## 1. Clone and Switch Branches
@@ -98,15 +98,41 @@ ls -l scripts | grep .sh
On Windows you will instead use the `.ps1` scripts (no chmod needed).
## 6. Run Lint / Basic Checks (Add Your Own)
## 6. Scaffold a Built-In Integration
Currently no enforced lint config is bundled, but you can quickly sanity check importability:
Use the integration scaffold command to create the initial Python package and
This guide will help you get started with Spec-Driven Development using Spec Kit.
This guide will help you get started with Spec-Driven Development using Spec Kit. Throughout, we illustrate each step with a running example: **Taskify**, a small team productivity platform.
> [!NOTE]
> All automation scripts now provide both Bash (`.sh`) and PowerShell (`.ps1`) variants. The `specify` CLI auto-selects based on OS unless you pass `--script sh|ps`.
> Automation scripts are provided as Bash (`.sh`), PowerShell (`.ps1`), and Python (`.py`) variants. Interactive `specify init` prompts you to choose one; non-interactive runs default to a shell variant for your OS. Pass `--script sh|ps|py` to select explicitly.
## Recommended Workflow
> [!NOTE]
> Commands are shown here in `/speckit.*` form, but the exact invocation depends on your agent. Some skills-based agents use `$speckit-*` (e.g. Codex, ZCode) or `/skill:speckit-*` (e.g. Kimi). Use whichever form your agent exposes — the steps are otherwise identical.
## Recommended Process
> [!TIP]
> **Context Awareness**: Spec Kit commands automatically detect the active feature based on your current Git branch (e.g., `001-feature-name`). To switch between different specifications, simply switch Git branches.
> **Context Awareness**: Spec Kit tracks the active feature by the feature directory recorded in `.specify/feature.json` (overridable with the `SPECIFY_FEATURE_DIRECTORY` environment variable). Commands resolve the feature from that state, **not** from the checked-out Git branch — no Git required. The opt-in **git** extension adds numbered feature branches (e.g. `001-feature-name`) for organizing work in version control, but the active feature is still whichever directory that state points to; `git checkout` alone does not change it. To point commands at a different feature, update `.specify/feature.json` (or set `SPECIFY_FEATURE_DIRECTORY`).
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:
After installing Spec Kit, each command below is a step in the process. Two paths are common:
**Shorter path** — for smaller features:
1.`/speckit.specify`
2.`/speckit.plan`
3.`/speckit.tasks`
4.`/speckit.implement`
5.`/speckit.converge`
**Full path** — for production features, adding `/speckit.clarify`, `/speckit.checklist`, and `/speckit.analyze` as quality gates:
1.`/speckit.constitution`
2.`/speckit.specify`
3.`/speckit.clarify`
4.`/speckit.plan`
5.`/speckit.checklist`
6.`/speckit.tasks`
7.`/speckit.analyze`
8.`/speckit.implement`
9.`/speckit.converge`
### Install Specify
**In your terminal**, install the CLI from PyPI (requires [uv](install/uv.md)), then initialize your project:
```bash
uv tool install specify-cli
specify init taskify # or: specify init . to use the current directory
```
`init` lets you pick your coding agent interactively, or pass it explicitly with `--integration` (e.g. `--integration copilot`).
> [!NOTE]
> Prefer `pipx`, one-time `uvx` runs, a pinned release, or an offline/air-gapped setup? See the [Installation Guide](installation.md) for all supported methods.
### Step 1: `/speckit.constitution` — set the ground rules
Establishes the project's guiding principles, which every later step is evaluated against. Run it once up front, passing your principles as arguments.
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.
### Step 1: Install Specify
**In your terminal**, run the `specify` CLI command to initialize your project:
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME> --script sh # Force POSIX shell
```
### Step 2: Define Your Constitution
**In your coding agent's chat interface**, use the `/speckit.constitution` slash command to establish the core rules and principles for your project. You should provide your project's specific principles as arguments.
```markdown
/speckit.constitution This project follows a "Library-First" approach. All features must be implemented as standalone libraries first. We use TDD strictly. We prefer functional programming patterns.
```
### Step 3: Create the Spec
**In the chat**, use the `/speckit.specify` slash command to describe what you want to build. Focus on the **what** and **why**, not the tech stack.
```markdown
/speckit.specify Build an application that can help me organize my photos in separate photo albums. Albums are grouped by date and can be re-organized by dragging and dropping on the main page. Albums are never in other nested albums. Within each album, photos are previewed in a tile-like interface.
```
### Step 4: Refine and Validate the Spec
**In the chat**, use the `/speckit.clarify` slash command to identify and resolve ambiguities in your specification. You can provide specific focus areas as arguments.
```bash
/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.
```markdown
/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.
```
### Step 6: Break Down, Analyze, and Implement
**In the chat**, use the `/speckit.tasks` slash command to create an actionable task list.
```markdown
/speckit.tasks
```
Validate cross-artifact consistency with `/speckit.analyze` before implementation:
```markdown
/speckit.analyze
```
Use the `/speckit.implement` slash command to execute the plan.
```markdown
/speckit.implement
```
> [!TIP]
> **Phased Implementation**: For complex projects, implement in phases to avoid overwhelming the agent's context. Start with core functionality, validate it works, then add features incrementally.
## Detailed Example: Building Taskify
Here's a complete example of building a team productivity platform:
### Step 1: Define Constitution
Initialize the project's constitution to set ground rules:
```markdown
/speckit.constitution Taskify is a "Security-First" application. All user inputs must be validated. We use a microservices architecture. Code must be fully documented.
```
### Step 2: Define Requirements with `/speckit.specify`
### Step 2: `/speckit.specify` — describe what to build
Creates the feature specification from a natural-language description. Focus on the **what** and **why**, not the tech stack.
```text
Develop Taskify, a team productivity platform. It should allow users to create projects, add team members,
assign tasks, comment and move tasks between boards in Kanban style. In this initial phase for this feature,
let's call it "Create Taskify," let's have multiple users but the users will be declared ahead of time, predefined.
I want five users in two different categories, one product manager and four engineers. Let's create three
different sample projects. Let's have the standard Kanban columns for the status of each task, such as "To Do,"
"In Progress," "In Review," and "Done." There will be no login for this application as this is just the very
first testing thing to ensure that our basic features are set up.
/speckit.specify Develop Taskify, a team productivity platform where predefined users create projects, assign tasks, comment, and move tasks across Kanban columns (To Do, In Progress, In Review, Done). Five users (one product manager, four engineers), three sample projects, no login for this first phase.
Use the `/speckit.clarify` command to interactively resolve any ambiguities in your specification. You can also provide specific details you want to ensure are included.
Asks targeted questions about anything underspecified and folds your answers back into the spec, so you're not planning on top of ambiguity. Run it before planning, optionally with a focus area.
```bash
/speckit.clarify I want to clarify the task card details. For each task in the UI for a task card, you should be able to change the current status of the task between the different columns in the Kanban work board. You should be able to leave an unlimited number of comments for a particular card. You should be able to, from that task card, assign one of the valid users.
```text
/speckit.clarify Focus on task card behavior — status changes, comment permissions, and user assignment.
```
You can continue to refine the spec with more details using `/speckit.clarify`:
### Step 4: `/speckit.plan` — choose the tech stack
```bash
/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.
Generates the design artifacts from the spec. This is where implementation detail belongs — provide your tech stack and architecture.
```text
/speckit.plan Use .NET Aspire with Postgres. The frontend is Blazor Server with drag-and-drop boards and real-time updates. Expose REST APIs for projects, tasks, and notifications.
```
### Step 4: Validate the Spec
### Step 5: `/speckit.checklist` — validate the spec
Validate the specification checklist using the `/speckit.checklist` command:
Generates a quality checklist — "unit tests for your requirements" — to confirm the spec is complete, clear, and consistent before you break the work down.
```bash
```text
/speckit.checklist
```
### Step 5: Generate Technical Plan with `/speckit.plan`
### Step 6: `/speckit.tasks` — break the work down
Be specific about your tech stack and technical requirements:
Generates an actionable, dependency-ordered `tasks.md` from the design artifacts.
```bash
/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 6: Define Tasks
Generate an actionable task list using the `/speckit.tasks` command:
Have your coding agent audit the spec, plan, and tasks with `/speckit.analyze` before implementation:
Reports conflicts, gaps, and ambiguities across `spec.md`, `plan.md`, and `tasks.md`. It's read-only — if it flags issues, fix them at the source and re-run before implementing.
```bash
```text
/speckit.analyze
```
Finally, implement the solution:
### Step 8: `/speckit.implement` — build it
```bash
Executes the tasks in `tasks.md` in dependency order. Run it once to build everything, or scope it to one phase at a time for large features.
Checks the codebase against the spec, plan, and tasks. If it finds gaps, it appends new tasks to `tasks.md`; run `/speckit.implement` and converge again until it reports converged. Otherwise you're done — proceed to review or open a PR.
```text
/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.
> For a full reference on each command — arguments, output, phased implementation, and how they interact — see [Agentic SDD](reference/agentic-sdd.md).
## Key Principles
@@ -201,6 +134,7 @@ Finally, implement the solution:
## Next Steps
- See the [Agentic SDD](reference/agentic-sdd.md) reference for full detail on every command
- Read the [complete methodology](https://github.com/github/spec-kit/blob/main/spec-driven.md) for in-depth guidance
- Check out [more examples](https://github.com/github/spec-kit/tree/main/templates) in the repository
- Explore the [source code on GitHub](https://github.com/github/spec-kit)
The **bug** extension adds a three-step bug triage process — assess, fix, and validate — that your coding agent runs alongside the core [Agentic SDD](agentic-sdd.md) process. Each bug lives in its own directory under `.specify/bugs/<slug>/`, with one Markdown report per stage.
> [!NOTE]
> Commands are written in `/speckit.bug.*` form throughout this page. The exact invocation depends on your agent — some skills-based agents use `$speckit-bug-*` (e.g. Codex, ZCode) or `/skill:speckit-bug-*` (e.g. Kimi). Substitute the form your agent exposes.
The bug extension is a bundled, opt-in extension. Install it before using these commands:
```bash
specify extension add bug
```
The three commands share a single handle — the **slug**, the per-bug directory name under `.specify/bugs/`. Supply it with `slug=<name>`; if omitted, `/speckit.bug.assess` asks for one (or generates a unique one in automated mode). Slugs are normalized to lowercase kebab-case. If an assessment already exists for a slug, an interactive run asks before overwriting it, while an automated run refuses and picks a new unique slug instead.
Triages a bug report — pasted text (such as a stack trace) or a URL (such as a GitHub issue) — against the codebase: it judges whether the report is a real bug, locates the suspected code paths, and proposes a remediation. This command is **read-only**: it writes only `assessment.md` and never modifies source code.
```text
/speckit.bug.assess "TypeError: cannot read properties of undefined (reading 'token') at /auth/callback"
Applies the remediation described in the assessment and records exactly what changed. This is the **only** bug command that edits source code, and it stays within the files listed in the assessment unless new evidence requires expanding scope (logged under **Deviations from Assessment**).
```text
/speckit.bug.fix slug=callback-token
```
Output: `.specify/bugs/<slug>/fix.md`.
## `/speckit.bug.test`
Validates the fix by re-running the reproduction and any added tests, then records the verification result — one of `verified`, `partial`, or `failed`. Like `assess`, it is **read-only** with respect to source code. Verdicts are never over-claimed: if the assessment listed a reproduction that wasn't actually exercised, the overall result is downgraded to `partial` rather than reported as `verified`.
The `/speckit.*` slash commands drive the core Spec-Driven Development (SDD) process — an **agentic process** your coding agent runs step by step. For a guided, end-to-end run see the [Quick Start Guide](../quickstart.md); this page is the detailed reference for each command — including arguments, output, and how they interact. For the philosophy behind the process, see [What is SDD?](../concepts/sdd.md). For bug triage, see [Agentic Bug Fix](agentic-bugfix.md).
The commands are designed to run in order, but only `/speckit.specify` is strictly required before `/speckit.plan`. The clarify, checklist, and analyze commands are quality gates you add for anything with meaningful ambiguity.
> [!NOTE]
> Commands are written in `/speckit.*` form throughout this page. The exact invocation depends on your agent — some skills-based agents use `$speckit-*` (e.g. Codex, ZCode) or `/skill:speckit-*` (e.g. Kimi). Substitute the form your agent exposes.
Creates or updates the project **constitution** — the guiding principles that every later phase is evaluated against — and keeps dependent templates in sync. Run it once up front and update it whenever your principles change. Pass the principles as arguments.
```text
/speckit.constitution This project follows a "Library-First" approach. All features must be implemented as standalone libraries first. We use TDD strictly. We prefer functional programming patterns.
```
## `/speckit.specify`
Creates or updates the feature **specification** from a natural-language description. Focus on the **what** and **why** — the user-facing behavior and goals — not the tech stack, which belongs in `/speckit.plan`.
```text
/speckit.specify Build an application that helps me organize photos into albums grouped by date, re-orderable by drag-and-drop on the main page, with a tile preview inside each album.
```
## `/speckit.clarify`
Asks up to five targeted questions about underspecified areas of the current spec and encodes your answers back into `spec.md`. Run it as many times as needed before planning, each time tackling a different area. Optionally pass a focus area as an argument.
```text
/speckit.clarify Focus on the task card behavior: status changes, comment limits, and who can be assigned.
```
Clarifying before planning keeps you from designing on top of ambiguity. If `/speckit.analyze` later surfaces requirement gaps, come back and run `/speckit.clarify` (or `/speckit.specify`) again.
## `/speckit.plan`
Runs the planning process to generate design artifacts from the spec. This is where implementation detail belongs — provide your tech stack, architecture, and technical constraints as arguments.
```text
/speckit.plan Use .NET Aspire with Postgres. The frontend is Blazor Server with drag-and-drop boards and real-time updates. Expose REST APIs for projects, tasks, and notifications.
```
## `/speckit.checklist`
Generates a quality checklist for the feature — think of it as **"unit tests for your requirements."** Rather than testing code, it checks whether the spec itself is complete, clear, unambiguous, and consistent (for example: "Are the drag-and-drop rules defined for every column?", "Is behavior specified for a deleted assigned user?").
Run it with no arguments for a broad pass, or pass a focus area to target one aspect:
```text
/speckit.checklist
```
```text
/speckit.checklist Focus on the Kanban board interactions and comment permissions.
```
Review the generated checklist. If it surfaces gaps, loop back to `/speckit.clarify` or `/speckit.specify` to tighten the spec before breaking the work down.
## `/speckit.tasks`
Generates an actionable, dependency-ordered `tasks.md` from the design artifacts. Tasks are organized into phases: **Setup**, **Foundational** (blocking prerequisites), then **one phase per user story** in priority order, and a final **Polish** phase for cross-cutting concerns. Tests are generated within a user story's phase when requested rather than as a separate phase, and tasks are marked for parallel execution where possible.
```text
/speckit.tasks
```
## `/speckit.analyze`
Performs a **read-only** cross-artifact consistency and quality analysis across `spec.md`, `plan.md`, and `tasks.md`, reporting conflicts, gaps, and ambiguities (for example a task with no matching requirement, or a plan choice that contradicts the spec). It never edits files — it produces a report and can optionally suggest remediations for you to approve.
```text
/speckit.analyze
```
Run it before implementing, while the artifacts can still be adjusted cheaply. If it surfaces issues, **return to the earlier step that owns them** and fix them at the source — `/speckit.specify` or `/speckit.clarify` for requirement problems, `/speckit.plan` for design problems, `/speckit.tasks` to regenerate the task list — then re-run `/speckit.analyze` until it comes back clean. You can also run `/speckit.analyze` again after implementation as an extra review.
## `/speckit.implement`
Executes the tasks in `tasks.md`, running each phase in dependency order and respecting parallel markers.
For a small feature, run it once to build everything:
```text
/speckit.implement
```
For a large feature, work in stages to avoid overwhelming the agent's context — scope each run with an argument, validate the result, then continue:
```text
/speckit.implement Implement only the Setup and Foundational phases: project scaffolding and the project/task data model with basic CRUD. Stop before the user-story features.
```
```text
/speckit.implement Now implement the Kanban board user story: drag-and-drop between columns.
```
Verify each stage works before moving to the next.
## `/speckit.converge`
Assesses the codebase against the feature's spec, plan, and tasks to confirm nothing was missed. It is **append-only**: it never edits or deletes code, and its only possible write is adding tasks to `tasks.md`. Run it only after `/speckit.implement` has run on the current `tasks.md`.
```text
/speckit.converge
```
It first prints a severity-graded findings summary, then resolves to one of two outcomes:
- **Converged** — no gaps found. `tasks.md` is left byte-for-byte unchanged and you'll see a clean result like `✅ Converged — the implementation satisfies the spec, plan, and tasks.` You're done; proceed to review or open a PR.
- **Tasks appended** — gaps found. Converge appends them as new tasks under a Convergence section in `tasks.md` and tells you how many. Run `/speckit.implement` again to complete them, then `/speckit.converge` once more. Each pass finds fewer items; repeat until it reports converged.
Bundles compose existing Spec Kit components — extensions, presets, workflows, and steps — into a single, versioned, installable unit. Where extensions and presets are primitives, a bundle is a curated stack that declares everything a team or role needs and installs it in one step through each component's own machinery. Bundles add no new runtime behavior of their own: they are a distribution and composition layer over the primitives you already use.
A bundle is described by a `bundle.yml` manifest and is discovered through the same catalog stack as other components. Installing a bundle resolves its declared components against pinned versions, checks for the single cross-bundle conflict point (the active integration), and applies each component idempotently with full provenance tracking so it can be cleanly removed or refreshed later.
## Search Available Bundles
```bash
specify bundle search [query]
```
| Option | Description |
| ----------- | ---------------------------- |
| `--offline` | Do not access the network |
| `--json` | Emit machine-readable JSON |
Searches all active catalogs for bundles matching the query. Without a query, lists every available bundle with its version, role, source, and a trust indicator (`verified` for org-curated catalog entries, `community` otherwise) so you can judge trust before installing.
Shows full metadata for a bundle along with the **fully expanded component set** it installs — every extension, preset, step, and workflow with its pinned version, plus preset priority and strategy. The output also includes a trust indicator (`verified` vs `community`) so you can judge trust before installing. This preview is the same plan `install` applies, so you can see exactly what will be added before committing. Foreseeable overlaps with components already provided by installed bundles are surfaced here as well.
| `--integration` | Override the integration used when initializing/installing |
| `--offline` | Do not access the network |
Installs a bundle's full component set through each primitive's machinery. The argument may be a catalog bundle id, or a local path to a built `.zip` artifact, a bundle directory, or a `bundle.yml` file; local sources install directly without consulting the catalog stack.
If the current directory is not yet a Spec Kit project, `install` initializes one first so a fresh checkout reaches a working state in a single command. `--integration` selects the integration when initializing a new project, and confirms the target when a bundle pins a specific integration but the project's active integration can't be determined (missing or unreadable `.specify/integration.json`). It does **not** override an already-initialized project's active integration: if a bundle targets a different integration than the project's, install aborts with no changes. Integration-agnostic bundles inherit the project's active integration. Installation is idempotent — components already present are skipped. On failure, no provenance record is written (a failed install records nothing), and the components installed during that run are removed on a best-effort basis — removal errors are swallowed, so partial on-disk state may remain.
| `--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.
> **Pin enforcement is install-time only.** Idempotency checks are id-based, not version-aware: a component that is already present is skipped during `install` without comparing its on-disk version to the manifest pin. Version pins are therefore guaranteed to be applied only when the bundler actually installs a component for the first time or refreshes it. Run `specify bundle update` to re-apply every owned component at its pinned version.
## Remove a Bundle
```bash
specify bundle remove <bundle_id>
```
Uninstalls only the components this bundle contributed, leaving any component that another installed bundle still needs in place (no collateral removals).
## List Installed Bundles
```bash
specify bundle list
```
| Option | Description |
| -------- | ---------------------------- |
| `--json` | Emit machine-readable JSON |
Lists the bundles installed in the project with their versions, component counts, and install timestamps.
Ensures the current directory is a Spec Kit project (initializing it idempotently if needed), then optionally installs the given bundle. Useful as an explicit one-step bootstrap for a new checkout.
| `--path` | Bundle directory or `bundle.yml` (default: current directory) |
| `--offline` | Verify references against bundled/installed components only |
Reports whether a `bundle.yml` is well-formed and whether every declared component reference resolves. References are checked against bundled components, the project's installed components, and — when online — the active catalogs. Validation fails only when a reference is definitively absent everywhere it could be checked: that is, when an active catalog is reachable and confirms the component is missing. References that cannot be verified — because validation is offline, or because a catalog is unreachable — are downgraded to warnings so authoring can continue, rather than failing the run.
| `--path` | Bundle directory (default: current directory) |
| `--output` | Output directory for the artifact |
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).
### List the Catalog Stack
```bash
specify bundle catalog list
```
Prints the active, priority-ordered catalog stack with each source's scope and install policy.
Registers a project-scoped catalog source and persists it.
### Remove a Catalog Source
```bash
specify bundle catalog remove <id_or_url>
```
Removes a project-scoped catalog source. Built-in default sources cannot be deleted.
> **Note:** `search` and `info` work anywhere — with no project they fall back to the built-in/user catalog stack. The remaining state-changing commands (`list`, `update`, `remove`, `catalog`) require a project already initialized with `specify init`. `install` and `init` will initialize a project on demand when run in an uninitialized directory.
| `--integration <key>` | AI coding agent integration to use (e.g. `copilot`, `claude`, `gemini`). See the [Integrations reference](integrations.md) for all available keys |
| `--integration-options` | Options for the integration (e.g. `--integration-options="--commands-dir .myagent/cmds"`) |
Creates a new Spec Kit project with the necessary directory structure, templates, scripts, and AI coding agent integration files.
> [!NOTE]
> The git extension is currently enabled by default during `specify init`.
> Starting in `v0.10.0`, it will require explicit opt-in. To add it after init, run `specify extension add git`.
> Git repository initialization and branching are managed by the **git extension**, which is not installed by default. Run `specify extension add git` after init to enable git workflows.
Use `<project_name>` to create a new directory, or `--here` (or `.`) to initialize in the current directory. If the directory already has files, use `--force` to merge without confirmation.
| `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/`.
## Check Installed Tools
```bash
specify check
```
Checks that required tools are available on your system: `git` and any CLI-based AI coding agents. IDE-based agents are skipped since they don't require a CLI tool.
Checks that CLI-based AI coding agents are available on your system. IDE-based agents are skipped since they don't require a CLI tool.
This command stays offline. If a command behaves like an older Spec Kit version or an expected CLI feature is missing, run `specify self check` to check whether your local CLI is behind the latest release.
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.
@@ -170,6 +171,65 @@ To set up configuration for a newly installed extension, copy the template:
Spec Kit stores project-level extension registration and hook configuration in:
```text
.specify/extensions.yml
```
The file contains installed extensions, global settings, and hooks that are surfaced before or after Spec Kit commands.
```yaml
installed:
- git
- my-extension
settings:
auto_execute_hooks:true
hooks:
before_implement:
- extension:git
command:speckit.git.commit
enabled:true
optional:true
priority:10
prompt:"Commit outstanding changes before implementation?"
description:"Auto-commit before implementation"
after_implement:
- extension:my-extension
command:speckit.my-extension.verify
enabled:true
optional:false
priority:5
description:"Run verification after implementation"
```
### Configuration fields
The top-level `installed` list records extensions installed in the project. The `settings` mapping stores project-wide extension settings, and `hooks` groups hook registrations by event.
`auto_execute_hooks` defaults to `true`, but is currently reserved and is not consulted when hooks are surfaced or invoked.
Each hook entry supports the following fields:
| Field | Description |
| --- | --- |
| `extension` | ID of the extension that registered the hook. |
| `command` | Extension command associated with the hook. |
| `enabled` | Whether the hook is active. Hooks with `enabled: false` are skipped. |
| `optional` | Whether the hook is optional. If `true`, the hook is presented with its `prompt` and can be skipped; if `false`, the hook is emitted as an automatic hook (includes `EXECUTE_COMMAND` markers). |
| `priority` | Priority metadata for the hook. Registered hook entries use integer values >= 1; entries installed from manifests default to `10` when no priority is declared. Current command templates surface hooks in their configured YAML order and do not sort them by `priority`. |
| `prompt` | Message shown when asking whether to run an optional hook. |
| `description` | Human-readable explanation of what the hook does. |
| `condition` | Optional expression evaluated by `HookExecutor` (using `config.<path>` or `env.<VAR>` with `is set`, `==`, or `!=`). Current command templates do not evaluate conditions and skip hooks with a non-empty condition. |
Hook event names identify when a hook is invoked. They generally use `before_<command>` or `after_<command>`, such as `before_implement`, `after_implement`, `before_tasks`, and `after_tasks`.
Extension manifests reject invalid hook priorities during installation. For existing `.specify/extensions.yml` entries, `HookExecutor.get_hooks_for_event()` sorts with `normalize_priority()`: missing values, booleans, non-numeric values rejected by `int()`, and values less than `1` fall back to `10`; numeric strings and finite floats are coerced with `int()`, while non-finite floats are unsupported and may fail instead of falling back.
`HookExecutor.get_hooks_for_event()` returns hooks ordered by `priority`, with lower values first. However, current command templates read hook lists directly and surface them in their configured YAML order rather than using priority ordering.
The Specify CLI supports a wide range of AI coding agents. When you run `specify init`, the CLI sets up the appropriate command files, context rules, and directory structures for your chosen AI coding agent — so you can start using Spec-Driven Development immediately, regardless of which tool you prefer.
The Specify CLI supports a wide range of AI coding agents. When you run `specify init`, the CLI sets up the appropriate command files and directory structures for your chosen AI coding agent — so you can start using Spec-Driven Development immediately, regardless of which tool you prefer.
## Supported AI Coding Agents
@@ -11,32 +11,36 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
| [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>` |
| [Factory Droid](https://docs.factory.ai/cli/getting-started/overview) | `droid` | Skills-based integration; installs skills into `.factory/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/` |
| [Grok Build](https://docs.x.ai/build/overview) | `grok` | Skills-based integration; installs skills into `.grok/skills` and invokes them as `/speckit-<command>` |
| [IBM Bob](https://www.ibm.com/products/bob) | `bob` | Skills-based integration by default; installs skills as `speckit-<command>/SKILL.md` under `.bob/skills/` and invokes them as `/speckit-<command>`. Pass `--integration-options="--legacy-commands"` to scaffold the deprecated Bob 1.x layout (`.bob/commands/*.md`) instead; that flag will be removed in a future release. Existing legacy installs can migrate with `specify integration upgrade bob --integration-options="--skills"`, which converts them to the skills layout and removes the old command files. If preset overrides are installed, the migration is rejected with an actionable error (preset artifacts cannot yet be reconciled across a layout change) — remove the preset(s), migrate, then reinstall them. |
| [Kilo Code](https://github.com/Kilo-Org/kilocode) | `kilocode` | Installs commands into `.kilo/commands`; legacy `.kilocode/workflows` installs remain supported as a registration fallback |
| [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 |
## List Available Integrations
@@ -45,10 +49,35 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
specify integration list
```
Shows all available integrations, which one is currently installed, and whether each requires a CLI tool or is IDE-based.
| `--catalog` | Also browse the catalog (built-in **and** community). Community integrations that are not built in are only shown here. |
Shows the built-in integrations, which one is currently installed, and whether each requires a CLI tool or is IDE-based.
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>`.
| `--integration-options` | Options for the integration |
Reinstalls an installed integration with updated templates and commands (e.g., after upgrading Spec Kit). Defaults to the default integration; if a key is provided, it must be one of the installed integrations. Detects locally modified files and blocks the upgrade unless `--force` is used. Stale files from the previous install that are no longer needed are removed automatically. Shared templates stay aligned with the default integration even when upgrading a non-default integration.
## Report Integration Status
```bash
specify integration status
specify integration status --json
```
Reports the current project's integration status without changing files. The
status report includes the default integration, installed integrations,
manifest paths, shared Spec Kit infrastructure health, unchecked manifests, and
the target integration for default-sensitive shared templates. The JSON form is
intended for CI and coding agents that need stable machine-readable status data;
it also reports the raw recorded integrations and the integration manifests that
were checked when state repair heuristics differ from the recorded file.
The command exits 0 when the report status is `ok` or `warning`; it exits 1
only when the report status is `error`. In JSON output, `multi_install_safe`
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`).
| `--type` | Scaffold template to use: `markdown` (default), `skills`, `toml`, or `yaml` |
## FAQ
### Can I install multiple integrations in the same project?
@@ -150,31 +255,38 @@ Spec Kit tracks one default integration in `.specify/integration.json` with `def
### Which integrations are multi-install safe?
An integration is multi-install safe when it uses isolated agent directories, a dedicated context file that does not collide with another safe integration, stable command invocation settings, and a separate install manifest. Shared Spec Kit templates remain aligned to the single default integration.
An integration is multi-install safe when it uses a static, unique agent root and command directory, stable command invocation settings, and a separate install manifest whose managed files do not overlap another safe integration. Registry tests enforce those path and manifest invariants. Shared Spec Kit templates remain aligned to the single default integration.
The Command directory column below lists the directory each integration installs its commands or skills into. Context-file targeting is a separate concern from integration multi-install safety: `multi_install_safe` is an integration declaration about command/skill paths, whereas the optional agent-context extension manages a per-agent context file (for example `AGENTS.md` or `CLAUDE.md`) and can even synchronize several anchors at once via its `context_files` setting. Multiple agents mapping to the same context file is expected there and does not affect whether an integration is multi-install safe; see the agent-context extension for details.
The currently declared multi-install safe integrations are:
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`.
Integrations that share a 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`.
### What happens to my changes when I uninstall or switch?
@@ -186,7 +298,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.
The Specify CLI (`specify`) manages the full lifecycle of Spec-Driven Development — from project initialization to workflow automation.
The Specify CLI (`specify`) manages the full lifecycle of Spec-Driven Development — from project initialization to workflow automation. This section is the detailed reference for the CLI's commands and primitives, plus the agentic `/speckit.*` processes your coding agent runs.
## Core Commands
@@ -10,7 +10,7 @@ The foundational commands for creating and managing Spec Kit projects. Initializ
## Integrations
Integrations connect Spec Kit to your AI coding agent. Each integration sets up the appropriate command files, context rules, and directory structures for a specific agent. Only one integration is active per project at a time, and you can switch between them at any point.
Integrations connect Spec Kit to your AI coding agent. Each integration sets up the appropriate command files and directory structures for a specific agent. Only one integration is active per project at a time, and you can switch between them at any point.
[Integrations reference →](integrations.md)
@@ -31,3 +31,25 @@ Presets customize how Spec Kit works — overriding command files, template file
Workflows automate multi-step Spec-Driven Development processes into repeatable sequences. They chain commands, prompts, shell steps, and human checkpoints together, with support for conditional logic, loops, fan-out/fan-in, and the ability to pause and resume from the exact point of interruption.
[Workflows reference →](workflows.md)
## Bundles
Bundles compose existing extensions, presets, workflows, and steps into a single, versioned, installable unit. Rather than adding new behavior, a bundle curates a stack of primitives — everything a team or role needs — and installs it in one step through each component's own machinery, with version pinning, conflict checks, and provenance tracking for clean updates and removal.
[Bundles reference →](bundles.md)
## Agentic Commands
The sections above cover primitives managed by the `specify` CLI. The following are the `/speckit.*` slash commands your coding agent runs step by step inside the editor — the agentic processes built on top of that foundation.
### Agentic SDD
The `/speckit.*` slash commands that drive the core Spec-Driven Development process your coding agent runs step by step: constitution, specify, clarify, plan, checklist, tasks, analyze, implement, and converge. Run them in order, adding the clarify/checklist/analyze quality gates for anything with meaningful ambiguity.
[Agentic SDD reference →](agentic-sdd.md)
### Agentic Bug Fix
The bundled **bug** extension adds a three-step bug triage process — assess, fix, and validate — with each bug tracked in its own directory under `.specify/bugs/`. Install it with `specify extension add bug`.
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.
| `--json` | Emit the run outcome as a single JSON object |
Runs a workflow from a catalog ID, URL, or local file path. Inputs declared by the workflow can be provided via `--input` or will be prompted interactively.
@@ -20,7 +21,25 @@ Example:
specify workflow run speckit -i spec="Build a kanban board with drag-and-drop task management" -i scope=full
```
> **Note:** All workflow commands require a project already initialized with `specify init`.
With `--json`, a single machine-readable object is printed instead of formatted text (the default output is unchanged when the flag is omitted):
```bash
specify workflow run my-pipeline.yml --json
```
```json
{
"run_id":"662bf791",
"workflow_id":"build-and-review",
"status":"paused",
"current_step_id":"review",
"current_step_index":0
}
```
`workflow_id` is the `workflow.id` declared inside the YAML, not the file name. The object is printed exactly as shown — pretty-printed with two-space indentation, on plain stdout with no Rich markup — so it always parses. While the workflow runs under `--json`, any progress a step would print (for example a gate prompt, or output from a prompt step's CLI subprocess) is redirected to stderr, so stdout carries only the JSON object. Read the object from stdout; leave stderr attached to the terminal or capture it separately.
> **Note:** Most workflow commands require a project already initialized with `specify init`. The exception is `specify workflow run <local-file.{yml,yaml}>`, which can run outside a project; in that case, run state is stored under the current directory's `.specify/workflows/runs/<run_id>/`.
## Resume a Workflow
@@ -28,14 +47,29 @@ specify workflow run speckit -i spec="Build a kanban board with drag-and-drop ta
| `--json` | Emit the resume outcome as a single JSON object |
Resumes a paused or failed workflow run from the exact step where it stopped. Useful after responding to a gate step or fixing an issue that caused a failure.
Supplied `--input` values are merged over the run's stored inputs and re-validated against the workflow's input types, then the blocked step is re-run with the updated values. This lets a run continue with information that only became available after it paused, or with a corrected value after a failure:
| `--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), a local YAML file, or a local directory containing `workflow.yml`.
## Workflow Overlays
Workflow overlays let a project extend or override an installed workflow without editing the installed `workflow.yml`. This keeps local customizations safe across `specify bundle update` or `specify workflow add` upgrades.
When `specify workflow run <workflow-id>` loads a workflow, the engine composes the base workflow with all enabled overlays for that workflow id. The result is validated like any other workflow definition.
### How Overlays Work
An overlay is a YAML file that declares a set of edit operations against the step list of a base workflow. Overlays use lower-wins precedence: higher priority numbers are applied first and lower numbers last. Equal-priority overlays are applied alphabetically by ID, with the last ID winning conflicts.
The recommended edit format uses the operation name as the key and the anchor step id as the value:
```yaml
id:"my-overlay"
extends:"speckit"
priority:10
enabled:true
edits:
- insert_after:implement
step:
id:run-lint
type:shell
run:"ruff check src/"
- replace:review-spec
step:
id:review-spec
type:gate
message:"Review the generated spec (overlay override)."
options:[approve, reject]
on_reject:abort
```
The explicit form is also supported:
```yaml
edits:
- operation:insert_after
anchor:implement
step:
id:run-lint
type:shell
run:"ruff check src/"
```
#### Fields
| Field | Required | Description |
| --- | --- | --- |
| `id` | yes | Identifier for this overlay. Used in `specify workflow overlay *` commands. Must be lowercase letters, digits, and hyphens only; no dots, underscores, path separators, or `overlays`. |
| `extends` | yes | The workflow id this overlay applies to. Uses the same safe-id format as `id`; `overlays`, `runs`, and `steps` are reserved. |
| `priority` | no | Integer; defaults to `10`. Lower values have higher precedence and win conflicts. Missing or invalid values fall back to `10`. |
| `enabled` | no | Boolean. Defaults to `true`. Disabled overlays are ignored. |
| `edits` | yes | Non-empty list of edit operations. |
#### Edit Operations
| Operation | `step` required | Effect |
| --- | --- | --- |
| `insert_after` | yes | Insert `step` immediately after the anchor step. |
| `insert_before` | yes | Insert `step` immediately before the anchor step. |
| `replace` | yes | Replace the anchor step with `step`. |
| `remove` | no | Remove the anchor step from the list. |
The `anchor` is the `id` of a step in the base workflow. Anchors are resolved recursively inside `then`, `else`, `steps`, `cases.*`, and `default` blocks, so nested base steps can also be targeted. Fan-out templates (`step` inside a `fan-out` step) are **not** valid anchors.
Step ids must not contain `:` — that character is reserved for engine-generated nested ids.
Validates the overlay file and copies it to `.specify/workflows/overlays/<extends>/<id>.yml`. `--priority` defaults to `10` and overrides the `priority` field in the file.
#### List Overlays
```bash
specify workflow overlay list <workflow-id>
```
Shows all overlays for the workflow, ordered by resolver precedence. Disabled overlays are marked as disabled in the listing and are ignored during workflow resolution.
Prints the layer stack (base + overlays) and the source attribution for each step after composition. Useful for debugging which overlay contributed or overrode a step.
### Example: Adding Automated Linting after Implementation
Given the built-in `speckit` workflow, create `project-overlay.yml`:
specify workflow run speckit -i spec="Build a kanban board"
```
The composed workflow will now run the full SDD cycle and execute `ruff check src/` automatically after the `implement` step.
### Example: Replacing a Gate
```yaml
id:"skip-plan-review"
extends:"speckit"
priority:5
edits:
- replace:review-plan
step:
id:review-plan
type:command
command:speckit.plan
input:
args:"{{ inputs.spec }}"
```
Lower priority values have higher precedence. Change this overlay to `priority: 5` if it must win a conflict with the `add-lint` overlay above. It replaces the `review-plan` gate with a non-interactive command.
### Interaction with Bundles and Updates
`specify workflow add <local-directory>` installs `workflow.yml` from the local directory into `.specify/workflows/<id>/`.
When an installed workflow is refreshed or reinstalled, project overlays in `.specify/workflows/overlays/<id>/` are preserved because they live outside the installed workflow directory.
### Limitations
- Overlays operate on the step list only. They cannot change workflow metadata (name, description, inputs, `requires`) or expression logic.
- Fan-out templates cannot be used as anchors.
- An overlay that targets a step id that does not exist in the base workflow will raise a validation error when the workflow is resolved.
- Overlays cannot target steps added by other overlays.
- Overlays cannot add new inputs or change the input schema of the base workflow.
## 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
@@ -68,9 +308,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.
@@ -228,6 +469,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 |
@@ -236,6 +478,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:
@@ -245,8 +489,10 @@ Steps can reference inputs and previous step outputs using `{{ expression }}` sy
| `inputs.spec` | Workflow input values |
| `steps.specify.output.file` | Output from a previous step |
| `item` | Current item in a fan-out iteration |
| `context.run_id` | Current workflow run ID |
| `context.workflow_dir` | Resolved absolute path to the workflow source directory. Empty string for string-loaded workflows. |
Available filters: `default`, `join`, `contains`, `map`.
Available filters: `default`, `join`, `contains`, `map`, `from_json`.
Example:
@@ -256,6 +502,40 @@ args: "{{ inputs.spec }}"
message:"{{ status | default('pending') }}"
```
### Interpolation and shell safety
Expressions are resolved by **plain string substitution** — the value of `{{ ... }}` is spliced into the surrounding text exactly as-is, with no quoting or escaping added. That is convenient for building `args` and `message` strings, but it has an important consequence for `shell` steps: a `run` field is handed to the system shell (`/bin/sh -c` on POSIX), so any interpolated value is interpreted as **shell syntax**, not just data.
If an interpolated value can contain characters like `;`, `|`, `&`, `$( )`, backticks, or quotes, it can change or extend the command that actually runs. This matters most when the value is not fully under the workflow author's control:
- **Workflow `inputs.*`** — supplied by whoever runs the workflow.
- **A prior step's output**, e.g. `{{ steps.plan.output.stdout }}` — for a `prompt` step this is **text produced by the AI agent**, which can in turn be influenced by files, tickets, or web content the agent read. Treat agent output as untrusted when it flows into a `shell` step.
There is **no shell-escaping filter** in the expression language and **no sandbox** around a `shell` step, so none of the practices below can be treated as a guarantee that a hostile value is neutralised. The only reliable control is to constrain what an interpolated value *can* be, and to keep values you cannot constrain out of `run` fields entirely. Scrutinise every `run` field that interpolates a value you do not control, and at minimum:
- **Constrain the value at the source with `enum`/an allowlist.** When `inputs.*` feeds a `run` field, restrict it to a fixed set of known-safe values so a caller cannot supply arbitrary shell text at all. This is the strongest control the engine offers — prefer it over any downstream mitigation.
```yaml
inputs:
target:
type: string
enum: [staging, production] # caller cannot inject arbitrary text
```
- **Keep unconstrained values out of `run`.** If a value cannot be constrained to an allowlist — most agent/`prompt` output — do not interpolate it into a `run` field. Branch on it with `if`/`switch` against fixed conditions, or act on it in a `command`/`prompt` step rather than a shell command built from it.
- **Quoting is not a security boundary.** Surrounding a substitution with quotes (`'{{ inputs.x }}'`) helps the shell treat a *trusted* value as a single argument and avoids word-splitting on spaces, but a value that itself contains the matching quote character can still break out and inject shell syntax. Quote for correctness on constrained values; never rely on quoting to make an *unconstrained* substitution safe.
- **Gates do not inspect the next step, and `message` is printed verbatim.** A `gate` step renders only its own `message`/`show_file` — it does not display, resolve, or sanitise the command that follows it, and approval never neutralises an injectable interpolation. Do **not** interpolate raw untrusted data into `message`: it is printed as-is with no control-character stripping, so agent or caller output could inject terminal/ANSI escapes that alter or hide the approval prompt. Keep `message` to trusted, constrained text, and surface untrusted material for review via `show_file` instead — its path and contents are control/ANSI-stripped before display.
A `shell` step is an arbitrary-command primitive by design; these practices reduce exposure and keep *which* command runs under the author's control, but they do not eliminate the risk of interpolating values you do not fully control.
## Shell Step Environment Variables
Shell steps automatically receive the following environment variables:
| Variable | Description |
| -------- | ----------- |
| `SPECKIT_WORKFLOW_DIR` | Resolved absolute path to the workflow source directory (same value as `{{ context.workflow_dir }}`). Not set when the workflow has no source path. |
| **CLI Tool Only** | `uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git@vX.Y.Z` | Get latest CLI features without touching project files |
| **CLI Tool Only (pipx)** | `pipx install --force git+https://github.com/github/spec-kit.git@vX.Y.Z` | Reinstall/upgrade a pipx-installed CLI to a specific release |
| **Project Files** | `specify init --here --force --integration <your-agent>` | Update slash commands, templates, and scripts in your project |
| **CLI Tool (recommended)** | `specify self upgrade` | Latest stable release, in place. Auto-detects whether you installed via `uv tool` or `pipx`. |
| **CLI Tool — pin a version** | `specify self upgrade --tag vX.Y.Z[suffix]` | Upgrade to a specific release tag instead of the latest stable. Suffixes are limited to dev, alpha/beta/rc, and/or build metadata forms. |
| **CLI Tool — manual fallback** | `uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git@vX.Y.Z` | When `specify self upgrade` isn't available (older installs) or when you want explicit control. |
| **CLI Tool — manual fallback (pipx)** | `pipx install --force git+https://github.com/github/spec-kit.git@vX.Y.Z` | Same as above, for pipx installs. |
| **Project Files** | Run `specify integration upgrade <key>`, then `specify extension update` | Refresh installed integration files and extensions in your project |
| **Both** | Run CLI upgrade, then project update | Recommended for major version updates |
---
@@ -19,12 +21,32 @@
The CLI tool (`specify`) is separate from your project files. Upgrade it to get the latest features and bug fixes.
Before upgrading, you can check whether a newer released version is available:
### Recommended: `specify self upgrade`
The CLI ships with two self-management commands that handle the common case automatically:
```bash
# Check whether a newer release is available (read-only — does not modify anything)
specify self check
# Preview what would run, without actually upgrading
specify self upgrade --dry-run
# Upgrade in place to the latest stable release (auto-detects uv tool vs pipx install)
specify self upgrade
# Or pin a specific release tag (replace vX.Y.Z[suffix] with the tag you want)
specify self upgrade --tag vX.Y.Z[suffix]
```
Bare `specify self upgrade` executes immediately, matching the no-prompt behavior of commands like `pip install -U` and `npm update`. The CLI classifies your runtime into one of: `uv tool`, `pipx`, `uvx (ephemeral)`, source checkout, or unsupported. Only `uv tool` and `pipx` are upgraded automatically; for `uv tool` installs, it runs `uv tool install specify-cli --force --from <git ref>` under the hood so pinned release tags work. The other paths print path-specific guidance and exit 0 without touching anything.
Pinned tags must start with `vMAJOR.MINOR.PATCH`. Optional suffixes are limited to dev, alpha/beta/rc, and/or build metadata forms such as `v1.0.0-rc1`, `v0.8.0.dev0`, `v0.8.0+build.42`, or the combination `v1.0.0-rc1+build.42`; branch names, hash refs, `latest`, and bare versions without `v` are rejected.
Set `SPECIFY_UPGRADE_TIMEOUT_SECS` to cap how long the installer subprocess may run (default: no timeout — interrupt with `Ctrl+C` if needed). If that internal timeout fires, `specify self upgrade` exits 124 and reports that it timed out while waiting for the installer subprocess, including the configured timeout and manual retry command. A real installer exit code 124 is propagated with `Upgrade failed. Installer exit code: 124.`, so scripts should treat exit 124 as ambiguous and inspect the message when they need to distinguish the two cases.
If your installed CLI is older than the release that introduced `specify self upgrade`, use the manual equivalents below. These commands are also useful when you want explicit control over the installer command.
### If you installed with `uv tool install`
Upgrade to a specific release (check [Releases](https://github.com/github/spec-kit/releases) for the latest tag):
# Confirms the CLI is working and shows installed tools
specify check
# Confirms the installed version against the latest GitHub release
specify self check
```
This shows installed tools and confirms the CLI is working. Use `specify version` to confirm which persistent CLI version is currently on your `PATH`.
`specify check` shows the surrounding tool environment; `specify self check` is read-only and tells you whether you're now on the latest release (`Up to date: X.Y.Z`) or if a newer one became available between releases.
---
## Part 2: Updating Project Files
When Spec Kit releases new features (like new slash commands or updated templates), you need to refresh your project's Spec Kit files.
When Spec Kit releases new features (like new slash commands, updated templates, or extension changes), you need to refresh the Spec Kit files that were installed into your project.
### What gets updated?
Running `specify init --here --force` will update:
For existing Spec Kit projects, use the manifest-aware upgrade path first:
- ✅ **Managed shared scripts and templates** (`.specify/scripts/`, `.specify/templates/`) when they are unchanged from the previous managed copy
- ✅ **Installed extensions** when you run `specify extension update`
The integration upgrade command uses the install manifest to detect local edits. If a managed integration file was modified after install, the command stops and asks you to inspect the change or rerun with `--force`.
### What stays safe?
These files are **never touched** by the upgrade—the template packages don't even contain them:
These files are **never touched** by the manifest-aware integration/extension upgrade path:
- ✅ **Your constitution** (`.specify/memory/constitution.md`) when using `specify integration upgrade`
- ✅ **Your source code** - **CONFIRMED SAFE**
- ✅ **Your git history** - **CONFIRMED SAFE**
The `specs/` directory is completely excluded from template packages and will never be modified during upgrades.
### Update command
### 1. Check installed integrations
Run this inside your project directory:
```bash
specify integration status
```
This reports the default integration, all installed integrations, and any modified or missing managed files. You can also inspect `.specify/integration.json`; installed integrations are listed under `installed_integrations`.
### 2. Upgrade each installed integration
Run this inside your project directory:
```bash
specify integration upgrade <key>
```
Replace `<key>` with an installed integration key such as `copilot`, `claude`, or `codex`. In projects with multiple installed integrations, run the command once per installed key.
**Example:**
```bash
specify integration upgrade claude
specify integration upgrade codex
```
See the [integration reference](reference/integrations.md#upgrade-an-integration) for options such as `--script`, `--integration-options`, and `--force`.
### 3. Update installed extensions
Run:
```bash
specify extension update
```
With no extension argument, this updates all installed extensions. Use `specify extension update <extension-id-or-name>` to update only one extension. See the [extensions reference](reference/extensions.md#update-extensions) for details.
### Fallback: re-run init
If a project predates manifests, has missing integration metadata, or needs a broader recovery, you can still re-run init:
Replace `<your-agent>` with your AI coding agent. Refer to this list of [Supported AI Coding Agent Integrations](reference/integrations.md)
**Example:**
```bash
specify init --here --force --integration copilot
```
### Understanding the `--force` flag
Without `--force`, the CLI warns you and asks for confirmation:
```text
Warning: Current directory is not empty (25 items)
Template files will be merged with existing content and may overwrite existing files
Proceed? [y/N]
```
With `--force`, it skips the confirmation and proceeds immediately. It also **overwrites shared infrastructure files** (`.specify/scripts/` and `.specify/templates/`) with the latest versions from the installed Spec Kit release.
Without `--force`, shared infrastructure files that already exist are skipped — the CLI will print a warning listing the skipped files so you know which ones were not updated.
**Important: Your `specs/` directory is always safe.** The `--force` flag only affects template files (commands, scripts, templates, memory). Your feature specifications, plans, and tasks in `specs/` are never included in upgrade packages and cannot be overwritten.
---
Use this as an escape hatch rather than the default project-file upgrade path. It refreshes the selected integration and shared project scaffolding, but it does not use the same per-integration manifest checks before overwriting files.
## ⚠️ Important Warnings
### 1. Constitution file will be overwritten
### 1. Constitution file and memory customizations
**Known issue:**`specify init --here --force` currently overwrites`.specify/memory/constitution.md` with the default template, erasing any customizations you made.
`specify integration upgrade <key>` does not update `.specify/memory/constitution.md`.
**Workaround:**
The fallback `specify init --here --force --integration <your-agent>` path also preserves an existing `.specify/memory/constitution.md`; if the file is missing, init creates it from the current constitution template. You do not need a constitution backup/restore step for the manifest-aware upgrade path.
`specify integration upgrade <key>` blocks when manifest-tracked integration files were modified locally, unless you pass `--force`.
Or use git to restore it:
```bash
# After upgrade, restore from git history
git restore .specify/memory/constitution.md
```
### 2. Custom script or template modifications
If you customized files in `.specify/scripts/` or `.specify/templates/`, the `--force` flag will overwrite them. Back them up first:
Shared scripts and templates are refreshed when they still match the previously recorded managed copy. Local customizations are preserved unless you explicitly use a force/refresh option that overwrites them. If you customized files in `.specify/scripts/` or `.specify/templates/`, commit or back them up first:
It **only** skips running `git init` and creating the initial commit.
### Working without Git
If you use `--no-git`, you'll need to manage feature directories manually:
**Set the `SPECIFY_FEATURE` environment variable** before using planning commands:
Projects that do not use Git can still work with Spec Kit by setting `SPECIFY_FEATURE_DIRECTORY` to the feature directory path before planning commands:
This tells Spec Kit which feature directory to use when creating specs, plans, and tasks.
**Why this matters:** Without git, Spec Kit can't detect your current branch name to determine the active feature. The environment variable provides that context manually.
Alternatively, run the `/speckit.specify` command which creates `.specify/feature.json` automatically.
---
@@ -310,29 +303,32 @@ This tells Spec Kit which feature directory to use when creating specs, plans, a
- Some agents need workspace restart or cache clearing
### "I lost my constitution customizations"
### "Will init overwrite my constitution customizations?"
**Fix:** Restore from git or backup:
Current `specify init --here --force` preserves an existing `.specify/memory/constitution.md`; it creates the file from the template only when it is missing.
If you previously lost constitution changes through an older workflow or manual replacement, restore from git or backup:
**Prevention:** Always commit or back up `constitution.md` before upgrading.
**Prevention:** Use `specify integration upgrade <key>` for routine project-file updates. If you need the fallback `specify init --here --force` path, commit first so you can review the full diff afterward.
### "Warning: Current directory is not empty"
@@ -356,10 +352,10 @@ This warning appears when you run `specify init --here` (or `specify init .`) in
- **Fallback recovery:** `specify init --here --force` when integration metadata is missing or the manifest-aware path cannot be used
- **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:**
@@ -440,10 +441,13 @@ Once you've run `specify init`, the slash commands (like `/speckit.specify`, `/s
ls -la .github/prompts/
# For Claude
ls -la .claude/commands/
ls -la .claude/skills/
# For Pi
ls -la .pi/prompts/
# For Oh My Pi
ls -la .omp/commands/
```
2. **Restart your IDE/editor completely** (not just reload window)
- **Value**: A single hook mapping, or a list of hook mappings to register multiple commands on one event
- **Description**: Hooks that execute at lifecycle events
- **Events**: Defined by core spec-kit commands
- **Ordering**: Within an event, hooks run by ascending `priority` (integer ≥ 1, default 10; lower runs first; equal priorities keep authoring order via a stable sort)
---
@@ -535,7 +543,9 @@ Examples:
### Hook Definition
**In extension.yml**:
Each event accepts either a single hook mapping or a list of mappings. A list registers multiple commands on the same event.
**Single mapping (in extension.yml)**:
```yaml
hooks:
@@ -547,6 +557,24 @@ hooks:
condition: null
```
**List of mappings with priority**:
```yaml
hooks:
after_plan:
- command: "speckit.my-ext.verify"
priority: 5
optional: false
description: "Verify the plan"
- command: "speckit.my-ext.report"
priority: 10
optional: true
prompt: "Generate the report?"
description: "Generate a report from the plan"
```
Within a single manifest list, a repeated `command` is deduped as "last wins" and moved to the end, so it also breaks equal-priority ties in authoring order.
Each event accepts a single hook object or a list of hook objects (multiple commands on one event).
Hook object:
-`command`: Command to execute (typically from `provides.commands`, but can reference any registered command)
-`priority`: Run order within the event (integer ≥ 1, default 10; lower runs first; equal priorities keep authoring order)
-`optional`: If true, prompt user before executing
-`prompt`: Prompt text for optional hooks
-`description`: Hook description
@@ -249,6 +252,7 @@ Use standard Markdown with special placeholders:
-`$ARGUMENTS`: User-provided arguments
-`{SCRIPT}`: Replaced with script path during registration
-`__SPECKIT_COMMAND_<NAME>__`: Replaced with the invocation of another command, rendered using the active integration's separator (see [Referencing other commands](#referencing-other-commands))
**Example**:
@@ -264,6 +268,40 @@ echo "Running with args: $args"
```
````
### Referencing other commands
A command body is a *template* that Spec Kit renders once per agent. Different agents invoke commands with different surface syntax — for example `/speckit.plan` (dot separator) or `/speckit-plan` (hyphen separator). Some agents also use different prefixes in skills mode (e.g. Kimi `/skill:speckit-plan`, Codex/ZCode `$speckit-plan`). So when you reference a sibling command from a body, **do not hard-code a literal invocation** like `/speckit.my-ext.prepare`. A literal is correct for exactly one agent and breaks on the rest.
Instead use the agent-neutral token `__SPECKIT_COMMAND_<NAME>__`. Spec Kit resolves it to a `/speckit<separator>...` invocation using the active integration's `invoke_separator` (and integrations may post-process that further in skills output).
Encode the command name in upper case, dropping the `speckit.` prefix and turning each dotted segment separator into an underscore:
The resolver maps each underscore back to the active agent's separator, so use tokens to reference commands whose name segments are single words. (Command names are dotted segments like `git.commit`; the token scheme rebuilds those dots and does not carry hyphens within a segment.)
**Example** — a command body that points the user at the next step:
```markdown
Once the assessment exists, the next step is `__SPECKIT_COMMAND_BUG_FIX__ slug=<slug>`.
```
This renders as `/speckit.bug.fix slug=<slug>` for a slash-based agent, `/speckit-bug-fix slug=<slug>` for a skills-based agent, and so on — the author writes it once and it stays portable. The first-party `bug` and `git` extensions use this token exclusively; see `extensions/bug/commands/` for working examples.
> **Current limitation — skills mode.** Token resolution runs in the
> command-rendering path (`CommandRegistrar`), so it applies when an extension
> installs *command files*. It does **not** yet run when an extension is
> registered as *skills* for a skills-based agent: `_register_extension_skills`
> resolves placeholders and post-processes content but never calls
> `resolve_command_refs`, so a `__SPECKIT_COMMAND_<NAME>__` token reaches
> agents such as Codex, ZCode, and Kimi verbatim in that mode. Until that
> rendering step lands, prefer the token for command-file extensions and avoid
> relying on it inside skill bodies destined for skills-based agents.
### Script Path Rewriting
Extension commands use relative paths that get rewritten during registration:
@@ -655,6 +693,23 @@ hooks:
description: "Analyze tasks after generation"
```
Multiple commands on one event, ordered by `priority` (lower runs first):
```yaml
# extension.yml
hooks:
after_plan:
- command: "speckit.my-ext.verify"
priority: 5
optional: false
description: "Verify the plan"
- command: "speckit.my-ext.report"
priority: 10
optional: true
prompt: "Generate the report?"
description: "Generate a report from the plan"
```
---
## Troubleshooting
@@ -667,7 +722,7 @@ hooks:
**Error**: `Extension requires spec-kit >=0.2.0`
- **Fix**: Update spec-kit with `uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git`. The bare `specify-cli` package on PyPI is a different, unrelated project — installing it without `--from git+...` will give you a stub CLI that does not include `extension`, `preset`, or other spec-kit commands.
- **Fix**: Upgrade Spec Kit using the [Upgrade Guide](../docs/upgrade.md). `uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git` remains available as a source-install fallback. If you installed from PyPI and want to stay on that route, follow the [PyPI upgrade guidance](../docs/install/pypi.md#upgrade).
This bundled extension manages the **coding agent context/instruction file** (e.g. `CLAUDE.md`, `.github/copilot-instructions.md`, `AGENTS.md`, `GEMINI.md`, …) for the active integration.
It owns the lifecycle of the managed section delimited by the configurable start/end markers (defaults: `<!-- SPECKIT START -->` / `<!-- SPECKIT END -->`).
It owns the lifecycle of the managed section delimited by the configurable start/end markers (defaults: `<!-- SPECKIT START -->` / `<!-- SPECKIT END -->`). For `.mdc` files, it also ensures the YAML frontmatter (the metadata block at the top of the file) contains `alwaysApply: true`. Otherwise, everything outside the managed section is untouched.
> NOTE: Spec Kit itself never touches your agent context file. This extension is the only thing that does, and it's opt-in: install it if you want the block kept in sync, skip it if you'd rather manage that file yourself.
## 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.
- **Refresh on demand** with `/speckit.agent-context.update`, or automatically through the hooks declared in `extension.yml` (`after_specify`, `after_plan`).
- **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; when it is absent, the file is never modified, and when it is disabled, its automatic hooks do not run.
- **Customize the markers** by editing `.specify/extensions/agent-context/agent-context-config.yml`([agent-context-config.yml](./agent-context-config.yml) in this repo) - 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** by running the `speckit.agent-context.update` command in your agent, or automatically through the hooks declared in [extension.yml](./extension.yml) (`after_specify`, `after_plan`).
## Installation
To install the extension, from the root of an initialized Spec Kit project, run:
```bash
specify extension add agent-context
```
## Disabling
```bash
specify extension disable agent-context
# Re-enable it
specify extension enable agent-context
```
While this extension is disabled (or not installed), nothing in Spec Kit creates, updates, or removes the managed block - the `__CONTEXT_FILE__` placeholder in any template is left as-is, and the extension's own config is never read.
| `speckit.agent-context.update` | Refresh the managed section in the agent context file with the current plan path. |
> NOTE: The command ID above is canonical. Invoke it using the syntax for your integration: `/speckit.agent-context.update` for dot-command integrations; `/speckit-agent-context-update` for hyphen/skills integrations (including Forge and Cline); `$speckit-agent-context-update` for Codex or ZCode in skills mode; or `/skill:speckit-agent-context-update` for Kimi.
## Configuration
All configuration flows through the extension's own config file at
# Path to the coding agent context file managed by this extension
context_file:CLAUDE.md
# Delimiters for the managed Spec Kit section
context_markers:
start:"<!-- SPECKIT START -->"
end:"<!-- SPECKIT END -->"
```
-`context_file` — the project-relative path to the coding agent context file, written by `specify init` and `specify integration install`.
-`context_markers.start` / `.end` — the delimiters around the managed section. Edit these to use custom markers.
All configuration flows through the extension's own config file at`.specify/extensions/agent-context/agent-context-config.yml` ([agent-context-config.yml](./agent-context-config.yml) in the repo).
## Requirements
The bundled update scripts require **Python 3** with **PyYAML** for YAML/upsert processing (PowerShell can also use `ConvertFrom-Yaml` when available).
PyYAML ships with the `specify` CLI and is normally available via the same `python3` interpreter. If a hook reports *"PyYAML is required … not available in the current Python environment"*, it means the system `python3` differs from the one used to install Spec Kit. To resolve, run:
PyYAML ships with the `specify` CLI and is normally available via the same `python3` interpreter. If a hook reports _"PyYAML is required … not available in the current Python environment"_, it means the system `python3` differs from the one used to install Spec Kit. To resolve, run:
```bash
pip install pyyaml
@@ -48,10 +58,6 @@ pip install pyyaml
/path/to/speckit-python -m pip install pyyaml
```
## Disable
## Issues
```bash
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()`).
For any other issues, please create an issue in the [official GitHub repo](https://github.com/github/spec-kit/issues).
# Path (relative to the project root) to the coding agent context file
# managed by this extension (e.g. CLAUDE.md, AGENTS.md,
# .github/copilot-instructions.md). Set automatically from the active
# integration and regenerated during `specify init` or integration switches.
# WHAT: The single agent context file relative to the project root (the directory containing .specify/). Absolute paths, backslash separators, and `..` path segments are rejected.
# REQUIREMENT: OPTIONAL. Use this if you want to manually specify a single context file. If you leave this entry blank, it will use the default context file for the coding agent you picked when you set up Spec Kit. See `agent-context-defaults.json` for the defaults.
# EXAMPLE: context_file: CLAUDE.md
context_file:""
# Delimiters for the managed Spec Kit section.
# Edit these to use custom markers.
# WHAT: List of agent context files relative to the project root (the directory containing .specify/). If you have both `context_file` and `context_files` filled, then this (`context_files`) takes precedence. Absolute paths, backslash separators, and `..` path segments are rejected.
# REQUIREMENT: OPTIONAL. Use this if your project requires you to keep multiple agent context files in sync.
# EXAMPLE:
# context_files:
# - AGENTS.md
# - CLAUDE.md
context_files:[]
# WHAT: Markers (delimiters) for the managed Spec Kit section. This extension injects information only between these markers.
# REQUIREMENT: OPTIONAL. Only change if you wish to have a custom marker name.
"_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.",
description: "Refresh the managed Spec Kit section in the coding agent context file"
description: "Refresh the managed Spec Kit section in coding agent context file(s)"
---
# Update Coding Agent Context
@@ -12,15 +12,16 @@ The script reads the agent-context extension config at
`.specify/extensions/agent-context/agent-context-config.yml` to discover:
-`context_file` — the path of the coding agent context file to manage.
-`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_file`is empty or the file cannot be located, the command reports nothing to do and exits successfully.
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.
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
}
# Reject absolute paths and '..' path segments in context_file
if([System.IO.Path]::IsPathRooted($ContextFile)){
Write-Warning"agent-context: context_file must be a project-relative path; got '$ContextFile'."
exit1
}
$cfSegments=$ContextFile-split'[/\\]'
if($cfSegments-contains'..'){
Write-Warning"agent-context: context_file must not contain '..' path segments; got '$ContextFile'."
exit1
foreach($ContextFilein$ContextFiles){
# Reject absolute paths, drive-qualified paths, backslash separators, and '..' path segments in context files
if($ContextFile -match'^[A-Za-z]:'){
Write-Warning"agent-context: context files must be project-relative paths; got '$ContextFile'."
exit1
}
if([System.IO.Path]::IsPathRooted($ContextFile)){
Write-Warning"agent-context: contextfiles must be project-relative paths; got '$ContextFile'."
exit1
}
if($ContextFile.Contains('\')){
Write-Warning"agent-context: context files must not contain backslash separators; got '$ContextFile'."
exit1
}
$cfSegments=$ContextFile-split'[/\\]'
if($cfSegments-contains'..'){
Write-Warning"agent-context: context files must not contain '..' path segments; got '$ContextFile'."
A five-stage assessment pipeline for Spec Kit that turns **any idea** into a defensible **go / needs-clarification / kill** decision *before* it enters Spec-Driven Development. It is the missing **discovery track** that sits in front of the SDD **delivery track** (`specify → clarify → plan → tasks → analyze → implement`).
Discovery answers *"is this worth building?"* Delivery answers *"how do we build it?"* Only ideas that survive assessment hand off to `/speckit.specify`.
## Overview
Each idea lives in its own directory under `.specify/assessments/<slug>/`, with one Markdown artifact per stage:
```
.specify/assessments/<slug>/
├── intake.md # speckit.assess.intake — capture the raw idea
The pipeline is a **funnel**: most ideas should be killed or parked before `shape`. Killing an idea with a documented reason is a successful outcome, not a failure.
C -.->|needs-clarification: revisit the named earlier stage| A
```
## Commands
| Command | Stage | Output |
|---------|-------|--------|
| `speckit.assess.intake` | Capture & normalize a raw idea (text, URL, ticket, or codebase pointer). | `intake.md` |
| `speckit.assess.research` | Gather users/market/prior-art/data evidence — and evidence *against* the idea. | `research.md` |
| `speckit.assess.define` | Define the problem: users, goals, non-goals, success metrics, cost of inaction. | `problem.md` |
| `speckit.assess.shape` | Shape 2–3 concept-level options with appetite and trade-offs; recommend one (or none). | `concept.md` |
| `speckit.assess.decide` | Score against criteria and render the verdict; hand `go` ideas to `/speckit.specify`. | `decision.md` |
Stages are meant to run in order but are not rigidly gated:
-`define` is the minimum viable stage and can run directly on user input (intake/research optional).
-`shape` requires `problem.md`.
-`decide` requires `problem.md`; a `go` verdict expects `concept.md` (otherwise it is downgraded to `needs-clarification`).
## Slug Conventions
A *slug* is the per-idea directory name under `.specify/assessments/`. It is the handle all five commands share.
- **User-provided**: normalized to lowercase kebab-case (e.g. `offline-mode`, `cut-onboarding-friction`). Preserved verbatim after normalization — no timestamps or numbers appended.
- **Asked for**: in interactive use, `speckit.assess.intake` asks for a slug when none is supplied, suggesting a kebab-case default derived from the idea.
- **Automated**: when no human is available, the agent generates a unique slug and never overwrites an existing assessment directory (appending `-2`, `-3`, … or a short date as needed).
- **Reuse from context**: later stages reuse the slug reported earlier in the same session, confirmed by the presence of the assessment directory.
## Installation
```bash
specify extension add assess
```
## Disabling
```bash
specify extension disable assess
specify extension enable assess
```
## Typical Flow
```bash
# 1. Capture an idea (pasted text, a URL, or "assess this repo")
/speckit.assess.intake "Let users work offline and sync when they reconnect"slug=offline-mode
# 2. Gather evidence — and reasons it might not be worth it
/speckit.assess.research slug=offline-mode
# 3. Define the actual problem
/speckit.assess.define slug=offline-mode
# 4. Shape 2–3 concept options with appetites
/speckit.assess.shape slug=offline-mode
# 5. Decide — go, clarify, or kill
/speckit.assess.decide slug=offline-mode
# → on "go", hand the decision.md handoff summary to /speckit.specify
```
## Handoff
`assess` is a **standalone pipeline you enter deliberately** — it registers no lifecycle hooks and never inserts itself into `/speckit.specify`. The only coupling runs forward and by choice: a `go` verdict from `/speckit.assess.decide` hands its `decision.md` summary to `/speckit.specify`. Discovery and specification stay separate processes.
## Guardrails
- Only `speckit.assess.*` commands write, and only inside `.specify/assessments/<slug>/`. **None of them modify source code** — solution design and implementation belong to the SDD lifecycle (`/speckit.specify` onward).
- Web content fetched during `intake`/`research` is treated as untrusted data, governed by an explicit URL Trust Policy (allowlisted public sources fetched freely; unknown hosts prompted or skipped; loopback/RFC1918/metadata endpoints refused).
- Evidence is never over-claimed: unsourced statements are tagged `ASSUMPTION`, and `research.md` always includes an *Evidence Against the Idea* section.
- Verdicts are never over-claimed: a `go` requires a valid problem, `adequate`+ evidence (never weak/unknown), and a shaped concept; otherwise the honest verdict is `needs-clarification`.
- Slugs are normalized to `[a-z0-9-]` and an empty result is rejected; before any read or write, each command also rejects symlinked path components and verifies the resolved path stays inside the project root — so an assessment can never escape `.specify/assessments/`, even in a crafted or cloned project.
- No command overwrites an existing artifact without confirmation; in automated mode it refuses.
## Relationship to Other Extensions
`assess` is deliberately the **generic, role-neutral** discovery track — usable by a founder, PM, BA, engineer, or designer. Richer or more specialized pre-SDD flows in the community catalog (e.g. product-lifecycle orchestrators, technical-discovery, intake-normalization, brownfield onboarding) can layer on top of or feed into it; `assess` aims to be the minimal, opinionated funnel that ends cleanly at the `/speckit.specify` handoff.
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.