When the extension config omits context_markers (or sets them blank),
relying on the built-in defaults, the Bash port aborted with "malformed
config parser output" and never updated the context file, while the
Python (`or DEFAULT_*`) and PowerShell (default-initialized) ports handled
it correctly.
The config parser prints three lines (context_files JSON, marker_start,
marker_end), captured via `_raw_opts="$(...)"`. Command substitution strips
trailing newlines, so blank marker lines collapse the output to fewer than
three, tripping the `(( ${#_opts_lines[@]} < 3 ))` guard and making the
DEFAULT_START/END substitution unreachable — the exact case it was written
for.
Require only the context_files line and default the marker lines to empty
(`${_opts_lines[1]:-}` / `${_opts_lines[2]:-}`) so the existing
DEFAULT_START/END fallback fills them in. Add a parity regression test with
blank markers (it fails on the old guard and passes with the fix).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The Python port of update-agent-context reintroduced a one-level plan
scan (specs/*/plan.md) in its mtime fallback, while the Bash and
PowerShell ports search recursively (specs/**/plan.md) per the fix for
issue #3024. The three ports were therefore not in parity: for nested
scoped layouts such as specs/<scope>/<feature>/plan.md, the Python port
found no plan and omitted the plan link from the managed context section.
Switch the fallback to `(root / "specs").rglob("plan.md")` and update the
module docstring to match the documented recursive-discovery contract.
Add a parity regression test covering the nested layout (it fails on the
one-level glob and passes with the recursive scan).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs(assess): clarify the pipeline works on an empty project
State explicitly in the README and intake command that the assess
pipeline requires no existing source code. An empty, freshly
initialized project and an existing codebase are equally valid
starting points — the input is just an idea (pasted text, a URL, a
ticket, or a codebase pointer).
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9df2615e-6a99-4cdc-b4b2-fc72029bc1d9
* docs(assess): distinguish empty project from no project
Clarify that assess still runs inside an initialized Spec Kit project
(writing under .specify/assessments/) — only existing source code is
optional. Reword 'no repo at all'/'need no repo' to 'need no existing
codebase' so users don't expect intake to work outside a Spec Kit
project.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9df2615e-6a99-4cdc-b4b2-fc72029bc1d9
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9df2615e-6a99-4cdc-b4b2-fc72029bc1d9
* 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>
* 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>
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).
* 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>
* 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.
* 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>
* 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>
* 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>
* 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>
* 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>
* 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>
`-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>
* 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>
* 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>
* 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>