* fix(taskstoissues): widen task-ID regex to match IDs longer than 3 digits
/speckit.converge assigns new IDs with T{M+1:03d}, where :03d is a
floor not a cap, so IDs already exceed three digits once a tasks.md
passes 999 entries. The dedup regex `\bT\d{3}\b` cannot match those
titles because the trailing \b can't fall between two digits, so
affected tasks are silently skipped instead of deduped or created.
* fix(taskstoissues): use command placeholder for the converge reference
The literal `/speckit.converge` added to the dedup step is not rewritten
by the dot-to-hyphen pass, so every skills-mode integration emitted a
SKILL.md containing dot notation and 19 integration tests failed. Use the
`__SPECKIT_COMMAND_CONVERGE__` placeholder, which resolves to
`/speckit.converge` or `/speckit-converge` per the agent's separator.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Return an actionable validation error when a workflow step type is a YAML list or mapping instead of raising during registry membership checks.
Assisted-by: OpenAI Codex (model: GPT-5, autonomous)
* Harden community submission workflow outputs
Restrict extension and preset submission PRs to the expected catalog and docs files.
* test: check community allowlists pairwise
---------
Co-authored-by: root <kinsonnee@gmail.com>
* fix(integrations): dispatch goose commands via `goose run` (#2416)
`YamlIntegration` never overrode `build_exec_args()`, so `GooseIntegration`
inherited the `IntegrationBase` no-op that returns `None`. Callers read `None`
as "this CLI is unavailable", so every workflow command/prompt step targeting
Goose reported `CLI not found or not installed` even with `goose` on PATH.
Reproduced with the agent CLI present on PATH (shutil.which stubbed to a real
path, subprocess.run stubbed):
amp -> completed argv=['amp', '-p', '/speckit.specify']
opencode -> completed argv=['opencode', 'run', '--command', 'speckit.specify']
goose -> FAILED "integration 'goose' CLI not found or not installed"
Implement `build_exec_args()` for Goose. Per the goose CLI docs there is no
`-p` flag; the non-interactive entry point is `goose run`, which takes
`-t/--text` for free-form text, `--recipe` for a stored recipe,
`--params KEY=VALUE` for recipe parameters, plus `--model` and
`--output-format`. Spec Kit installs its commands as Goose *recipes* under
`.goose/recipes/`, each declaring an optional `args` parameter (already
enforced by test_setup_declares_args_parameter_for_args_prompt), so a
`/speckit.<name> <rest>` invocation maps exactly onto
`--recipe <path> --params args=<rest>`. This mirrors `OpencodeIntegration`,
which maps the same leading slash-command onto opencode's `--command`.
The recipe path is derived from the same two sources `setup()` uses --
`config["folder"]` + `config["commands_subdir"]` and `command_filename()` --
so the dispatch target cannot drift from the installed file; a test asserts the
resolved `--recipe` path exists after `setup()`. Dotted extension commands
(`speckit.git.commit`) round-trip. Extra args are applied before the canonical
flags so Spec Kit's selection stays authoritative, matching opencode.
No behaviour change for other integrations, and `requires_cli` is untouched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(goose): only map the speckit. namespace onto --recipe
build_exec_args() treated every prompt starting with "/" as a Spec Kit
recipe. Because command_filename() unconditionally re-adds the "speckit."
prefix, a free-form slash prompt was silently promoted into a recipe run
against a file that was never installed:
/help -> --recipe .goose/recipes/speckit.help.yaml
/plan the sprint -> --recipe .goose/recipes/speckit.plan.yaml
/speckit. -> --recipe .goose/recipes/speckit..yaml
PromptStep passes arbitrary prompt: strings to build_exec_args, and both
/help and /plan are Goose's own session commands, so this is reachable.
Unlike opencode's --command or hermes' -s, which hand a bare name to the
agent's own resolver, --recipe is a path Spec Kit synthesizes -- so only
the namespace it can actually spell may take that branch.
Gate the branch on "/speckit." and fall through to -t otherwise. A bare
"/speckit." leaves no stem and also falls through.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(goose): stop asserting an argv that goose would reject
test_goose_extra_args_cannot_clobber_prompt_derived_recipe asserted that a
duplicated --recipe is merely reordered, on a "last value wins" premise.
That premise is wrong for goose: `goose run` is clap-derive based and
--recipe/--model/--output-format are single-value args without
args_override_self, so a duplicate makes goose exit with "cannot be used
multiple times" whichever side comes first. The test passed in pytest
while pinning a command line that cannot run.
Replace it with an ordering-parity test that asserts only what Spec Kit
actually controls: extra args precede the canonical flags (matching
opencode/codex/cursor-agent), and Spec Kit never emits a duplicate
single-value flag itself. Verified non-vacuous -- it fails if the
extra-args hook is moved after the canonical flags.
The ordering comment claimed precedence it cannot deliver; corrected to
state positional parity only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Test-FileExists / Test-DirHasFiles report their line with Write-Output and
ALSO return $true/$false — both on the Success stream. The callers piped
the whole call to `| Out-Null` to discard the boolean, which discarded the
report line with it, so text mode printed the header and nothing under it:
BEFORE (measured, powershell.exe -NoProfile -File ... -IncludeTasks):
FEATURE_DIR:...\specs\001-f
AVAILABLE_DOCS:
(2 lines)
AFTER:
FEATURE_DIR:...\specs\001-f
AVAILABLE_DOCS:
[OK] research.md
[FAIL] data-model.md
[FAIL] contracts/
[FAIL] quickstart.md
[FAIL] tasks.md
(7 lines)
The bash and Python twins both list every document under that header, so
the PowerShell variant silently returned less information for the same
inputs.
Filter out only the boolean, keeping the report lines. Adds the first
PowerShell text-mode test in this file (every existing PS test is -Json).
File stays ASCII-only (verified 0 non-ASCII bytes).
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Remove exists() check before open() and catch FileNotFoundError directly.
This prevents a race where the file is deleted between check and open,
while preserving the descriptive error message.
Review follow-up: feeding PyYAML the byte stream let its Reader honour
a UTF-16 BOM and accept a manifest yamlio.load_yaml rejects, so zip and
directory sources diverged. Decode raw as UTF-8 (UnicodeError ->
BundlerError 'Could not read ...') then parse, and cover a well-formed
UTF-16 manifest in the regression tests.
Assisted-by: GitHub Copilot (model: claude-fable-5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: log progress tracker refresh errors instead of silently swallowing
The bare 'except Exception: pass' in StepTracker._maybe_refresh()
completely hid rendering bugs in the Rich progress display. Now logs
at DEBUG level with full traceback for diagnostics.
* test: add regression test for StepTracker refresh error logging
- Test that _maybe_refresh logs exceptions instead of silently swallowing
- Verify diagnostic message and traceback are recorded in DEBUG logs
- Confirm tracker update completes normally despite refresh callback failure
Requested by Copilot in PR #3975
* fix(bundler): read the authoritative default_integration field
`active_integration()` resolves a project's integration with
data.get("integration") or data.get("id") or data.get("active")
and never looks at `default_integration` — which is the key the CLI
actually writes. `integration_state.set_default_integration` persists
`data["default_integration"] = integration_key`, and the canonical
reader in that module orders it the other way round:
key = state.get("default_integration") or state.get("integration")
So a project initialised by any current version of the CLI looks to the
bundler as though it has no active integration:
{"default_integration": "copilot"} -> None (expected "copilot")
{"integration": "copilot"} -> "copilot" (legacy alias)
That silently changes bundler behaviour that keys off the active
integration, including the FR-019 clash guard, which treats an
undeterminable integration differently from a known one.
Read `default_integration` first and keep the three legacy aliases as
fallbacks for projects initialised by older versions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(bundler): correct the justification for reading default_integration
Review catch: the comment cited a nonexistent
`integration_state.set_default_integration` and overstated the impact.
The real writer is `write_integration_json`, which persists BOTH
`integration` and `default_integration` (integration_state.py:248-250), so
a marker produced by the current CLI already resolved through the
`integration` alias. Measured:
{"integration": "copilot", "default_integration": "copilot"} -> 'copilot'
{"default_integration": "copilot"} -> 'copilot' (after fix)
Reword both the source comment and the test docstring: this is about which
field is authoritative when they disagree, plus resolving a marker that
carries only `default_integration` — not about every current project being
undetectable. The precedence itself still has its precedent, the canonical
reader at integration_state.py:199.
Behaviour unchanged; comments and docstrings only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: add Mistral Vibe integration with Claude parity
- Add VibeIntegration class with ARGUMENT_HINTS, user-invocable, disable-model-invocation
- Add comprehensive test suite matching Claude integration
- Support all Spec Kit workflows (py/sh/ps script types)
* fix: address Vibe integration issues and test cleanup
- Fix Vibe to use .vibe/hooks.toml with toml-vibe format instead of
ignored .vibe/settings.json, adding toml-vibe event handler
- Remove unsupported argument-hint injection (Vibe schema doesn't support it)
- Restructure test file to inherit from SkillsIntegrationTests mixin
- Remove all unused imports to pass Ruff F401 checks
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
* fix: add name field to Vibe hooks and fix toml regex patterns
- Add required 'name' field for each Vibe hook in hooks.toml
- Fix regex patterns in _merge_vibe_toml_fragment and _remove_vibe_toml_entries
to correctly match [[hooks]] blocks instead of [} characters
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
* fix: align Vibe hooks with HookConfig schema and drop stray devcontainer lock
- use Vibe's 'match' field (re:-prefixed regex translation) instead of
unsupported 'matcher'; emit only on tool hooks (rejected on post_agent)
- limit CANONICAL_TO_NATIVE to Vibe's three hook types (pre_tool,
post_tool, post_agent); unsupported events skip with a warning
- deduplicate generated hook names (Vibe drops duplicates by name)
- add behavioral tests for toml-vibe generation, merging, and teardown
- remove accidentally committed .devcontainer/devcontainer-lock.json
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: wrap Vibe hook stdout in structured JSON response envelope
Vibe parses any non-empty hook stdout as a JSON HookStructuredResponse;
plain text is reported as a hook failure and its output dropped. Add a
hook_specific_output envelope to the dispatcher (template and runtime)
that emits {"decision": "allow", "hook_specific_output":
{"additional_context": ...}} and declare it for all Vibe events:
post_tool injects the context, pre_tool/post_agent parse cleanly and
ignore it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: quote Vibe hook commands for cmd.exe on Windows hosts
Vibe launches hooks via asyncio.create_subprocess_shell, which is
%COMSPEC% (cmd.exe) on Windows — POSIX single-quoting is not quoting
there, so an interpreter or dispatcher path containing spaces made every
hook fail to start. Add a 'cmd' quoting target to _shell_quote
(double-quote when needed, embedded quotes doubled per MSVCRT argv
rules), resolve it host-side like 'host', and select it for Vibe when
generating on a Windows host.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: pin POSIX quoting target in Vibe test for Windows CI runners
test_posix_host_keeps_shlex_quoting asserts host (shlex) quoting, but on
a Windows runner _vibe_target_os() resolves to 'cmd' and the command is
double-quoted. Monkeypatch the target so the test exercises the POSIX
path on every platform.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* chore: bump version to 0.16.3
* chore: begin 0.16.4.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix: narrow bare except Exception in VS Code settings merge
Replace overly broad except Exception with (OSError, ValueError, KeyError)
to let programming errors like TypeError or AttributeError propagate while
still handling expected I/O and parse errors gracefully.
* test: verify programming errors propagate through handle_vscode_settings
The narrow exception change from 'except Exception' to
'except (OSError, ValueError, KeyError)' was not covered by a
regression test. Add a test that monkeypatches merge_json_files to
raise TypeError and verifies it propagates rather than being swallowed.
`specify preset list` now sorts installed presets by (priority, id) so the
printed order matches the actual resolution/composition order used by
PresetRegistry.list_by_priority(). Lower priority number = higher precedence;
ties are broken alphabetically by preset id. Adds a header and footer note
clarifying the ordering, updates the presets reference docs, and adds tests.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* Fix: scaffold self-contained namespaced preset commands (#4076)
Preset command templates named `speckit.<ns>.<cmd>` were silently dropped
whenever `.specify/extensions/<ns>/` was absent, while `speckit.<cmd>`
always scaffolded. The `_extension_installed_for_command` guard filtered
purely on name shape, conflating "override of an installed extension's
command" with "a preset shipping its own namespaced command." Because a
`type: command` template always ships its own body, such a command is
self-contained and must scaffold like any short-named command.
Remove the name-shape guard at all four call sites (registration, both
reconciliation passes, and skills). The reconciliation loop already skips
names that resolve to no layers (`if not layers: continue`), and the
composed-None branch still cleans up commands whose base layer disappeared.
Convert the command-mode "no base layer to compose onto" hard error into a
warn + skip, matching the existing behavior in _reconcile_composed_commands
so command-mode install and reconciliation stay consistent.
Update the two tests that encoded the old drop behavior to assert the new
consistent-scaffold contract, and add coverage proving 2-part and 3-part
preset commands scaffold identically with no extension installed.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cfc4f1ce-6acb-465a-aa7b-999f2e4197fb
* Skip uncomposable commands in skills mode too (PR #4082 review)
When _register_commands skips an uncomposable composition command (a
wrap/prepend/append with no base layer to compose onto — e.g. the command
it wraps comes from an uninstalled extension), install still passed the
full manifest to _register_skills. For a command-backed integration in
skills mode, _register_skills created the missing skill and fell back to
the raw preset body because no `.composed` file existed, materializing a
broken SKILL.md — a literal `{CORE_TEMPLATE}` for wrap, or just the
preset's own fragment for prepend/append. Previously the raise in
_register_commands aborted before skills ran, so this never surfaced.
Make _register_skills apply the same skip: for a composition-strategy
command with no `.composed` file, resolve the stack and skip when no base
exists (resolve_content is None). The skip is silent because
_register_commands already warned for the same command in the same pass.
Add a regression test proving an uncomposable wrap command renders no skill
and never leaks a literal {CORE_TEMPLATE} in 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: cfc4f1ce-6acb-465a-aa7b-999f2e4197fb
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cfc4f1ce-6acb-465a-aa7b-999f2e4197fb
`resolve_install_plan`'s two FR-019 guards are a truthiness test and an
`is None` test:
if active_integration and required != active_integration: # clash
if active_integration is None and not integration_explicit: # indeterminate
An empty string satisfies neither, so it falls through to
`effective_integration = required` and the bundle's pinned integration is
silently adopted — the exact outcome the docstring says the guard prevents
("resolution fails instead of silently adopting the bundle's required
integration").
active=None -> BundlerError: ... could not be determined
active='' (blank) -> effective_integration='copilot' <-- silent adopt
active='claude' -> BundlerError: ... targets integration 'copilot'
Normalise a blank value to None before the guards, and strip first to
match the writer (`integration_state.clean_integration_key`, which returns
`None` for empty/whitespace and strips otherwise) so a padded value is not
reported as clashing with its own unpadded form.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add Junie integration with dot-to-hyphen behavior, command formatting, and file transformations. Based on Cline Integration.
* Fix references to Cline in Junie integration and update class/test names for consistency.
* Fix references to Cline in Junie integration and update class/test names for consistency.
* Modified to generate correct formatting in junie
* fix: use bounded read for bundle download HTTP responses
The bundle download used unbounded resp.read() to read HTTP responses
into memory. A malicious or misconfigured catalog server could return
an arbitrarily large payload causing OOM.
Replace with read_response_limited() capped at MAX_DOWNLOAD_BYTES
(50 MiB), consistent with how other download paths in the codebase
enforce bounded reads.
Add regression test that monkeypatches MAX_DOWNLOAD_BYTES to 100 bytes
and verifies oversized responses are rejected.
* fix: remove duplicate import of MAX_DOWNLOAD_BYTES and read_response_limited
* fix(claude): make argument-hint injection fold-aware for long descriptions
ClaudeIntegration.inject_argument_hint spliced argument-hint: "..." as a
raw text line right after the first line starting with "description:".
When a description is long enough for the YAML dumper to fold it across
indented continuation lines, that splice landed inside the scalar,
producing invalid YAML (plain scalar) or silently absorbing the hint
into the description string (quoted scalar). This reproduces #3991 for
the case #3996 didn't cover: bundled core commands have no argument-hint
in their source frontmatter, so the structural apply_argument_hint path
is a no-op and this raw-text fallback is what actually runs.
Skip every continuation line of the description scalar (anything more
indented than the key itself) before inserting, so the new key always
lands after the whole scalar ends rather than in the middle of it.
Fixes#4044
* fix(claude): also skip unindented blank lines in description scalar
PyYAML serializes an embedded paragraph break ("\n\n") inside a quoted
description as unindented blank lines, not indented continuation
lines. inject_argument_hint only skipped indented lines, so it still
inserted argument-hint mid-scalar for multi-paragraph descriptions,
reproducing the #4044 failure modes. Skip blank lines too, and add a
regression test for the multi-paragraph case.
Catalog submission issues no longer auto-assign mnriem. The workflow now
only posts the team notification comment (cc @github/spec-kit-maintainers),
since GitHub issues cannot be assigned to a team. Renamed the workflow and
job to reflect its notification-only purpose.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63ae7e23-13e3-4a83-96b3-1178422aaa6e
Community extension, preset, and bundle submissions are validated by
label-triggered agentic workflows that only run once the corresponding
`*-submission` label is applied. On this public repo contributors cannot
apply that label themselves, so a maintainer applies it during issue
triage. Document this in the three submission issue templates, the preset
publishing guide, and correct the extension guide's inaccurate claim that
issues are "automatically labeled and assigned".
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3b6f6854-9be6-4b67-b5b9-34b19ededcb8
* chore: bump version to 0.16.2
* chore: begin 0.16.3.dev0 development
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Add Command Code integration to spec-kit
Adds `command-code` as a built-in skills-based integration so Spec Kit can
be installed into Command Code. Command Code loads agent skills from
`.commandcode/skills/speckit-<name>/SKILL.md` and invokes them in chat as
`$speckit-<command>`.
- New `CommandCodeIntegration` (SkillsIntegration) writing to
`.commandcode/skills/`; declared multi-install safe (static, isolated
agent root).
- Register in `_register_builtins()` and the integration catalog.
- Add `command-code` to `DOLLAR_SKILLS_AGENTS` so next-steps guidance
renders `$speckit-*` invocations.
- Tests: reuse `SkillsIntegrationTests` mixin plus a dollar-invocation
next-steps test; registry completeness updated.
- Docs: README and docs/reference/integrations.md (supported agents +
multi-install-safe table).
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Assisted-by: Command Code (autonomous)
* Fix issue template agent lists to include command-code
The runtime AGENT_CONFIG now includes command-code, but the GitHub issue
templates and the consistency test's expected key list were not updated,
failing test_issue_template_agent_lists_match_runtime_integrations.
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Assisted-by: Command Code (autonomous)
---------
Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
evaluate_condition() special-cases the strings "false"/"true" so that
`condition: "false"` behaves as a boolean, but it matches with
`result.lower()` and never strips.
The most common way a *string* reaches a condition is captured command
output, and the shell step stores stdout verbatim
(steps/shell/__init__.py:67 `"stdout": proc.stdout`). So `run: echo false`
resolves to "false\n", which matches neither branch and falls through to
`bool("false\n")` -> True:
'false' -> False
'false\n' -> True <-- bug
'false\r\n' -> True <-- bug
' false' -> True <-- bug
An `if` step therefore takes its `then` branch on a step that printed
"false", and `while`/`do-while` keep dispatching their body.
A workflow author cannot work around it: the registered filters are
default/join/map/contains/from_json — there is no `trim`.
`InitStep._resolve_bool` and both catalog readers already strip before
matching boolean text. `bool(result)` still sees the raw string, so no
non-boolean text changes truthiness.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`_parse_edit` reads `operation` straight from hand-edited YAML and then
does `if operation not in VALID_OPERATIONS`. `VALID_OPERATIONS` is a
frozenset, so that membership test hashes the value — and an unhashable
one raises:
operation={'insert_after': 'a'} -> TypeError: unhashable type: 'dict'
operation=['insert_after'] -> TypeError: unhashable type: 'list'
`validate_overlay_yaml`'s docstring promises "validation never raises",
and nothing upstream catches TypeError (layer_sources wraps only
YAMLError/OSError/UnicodeDecodeError; _commands catches only ValueError),
so the CLI dies with a raw traceback instead of reporting the error.
The trigger is an ordinary authoring mistake: nesting the recommended
shorthand form under the explicit key.
Every other field in the same function is isinstance-guarded first
(`anchor`, `step`, `step["id"]`); `operation` was the outlier. Check the
type first and return the message the function already uses for
`operation: None` / `operation: 7`.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: provision Python test deps for bug-test workflow
* test: anchor bug-test workflow domain assertions
Address CodeQL py/incomplete-url-substring-sanitization alerts (14-17)
by anchoring the PyPI domain assertions to their structural context:
the `network.allowed` YAML list items in the source and the quoted JSON
entries in the compiled lock. This defeats the incomplete-URL-substring
pattern and strengthens the test to confirm the domains are real
allowlist entries rather than incidental substrings.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b54442f-ccc5-4be1-a05c-b360889670e5
* fix: provision test deps without creating a project lock
Replace `uv sync --extra test` with `uv pip install --system -e ".[test]"`
in the bug-test provisioning step.
`uv sync` writes a root `uv.lock` (and `.venv`) into the working tree.
This repository intentionally has no `uv.lock`/`[tool.uv]` (uv.lock is
gitignored), so the sync produced an untracked lockfile before the agent
checks out the fix ref in Step 2. `uv pip install` installs the test
extra into the runner's Python without generating a project lock, keeping
the working tree clean before the fix checkout. The editable install
means the agent's `python3 -m pytest` runs against the checked-out fix
code. Recompiled the lock and updated the assertions accordingly.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b54442f-ccc5-4be1-a05c-b360889670e5
* chore(workflows): sync gh-aw action-pin metadata to latest across all workflows
Dependabot bumps the third-party action `uses:` pins (and header comments)
directly, but does not update gh-aw's own metadata: the per-file
`gh-aw-manifest` JSON blob and the shared `.github/aw/actions-lock.json`
pin cache. As a result the executing pins were already uniform and current
(checkout v7.0.1, setup-node v7.0.0) while the manifest/cache metadata still
recorded checkout v6.0.3 / setup-node v6.4.0.
This is a latent downgrade hazard: a plain `gh aw compile` reads the stale
cache and can silently revert the `uses:` lines back to the older pins,
undoing Dependabot's bumps and breaking lockstep.
Sync all four pin surfaces (uses / header comment / manifest / cache) to the
current pins so every workflow agrees and a future recompile is a no-op:
- actions-lock.json: checkout v6.0.3 -> v7.0.1, setup-node v6.4.0 -> v7.0.0,
and add the setup-python v7.0.0 + setup-uv v9.0.0 entries now used by
bug-test.
- gh-aw-manifest blobs in the 5 non-bug-test lock files: checkout + setup-node
bumped to match their own uses lines (bug-test was already current).
No workflow body changes; only pin metadata. `uses:` pins are unchanged.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b54442f-ccc5-4be1-a05c-b360889670e5
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)
---------
Co-authored-by: root <kinsonnee@gmail.com>
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b54442f-ccc5-4be1-a05c-b360889670e5
* fix(bundle): escape Rich markup in bundle CLI error and status output
`specify bundle`'s `_fail` helper interpolated its message straight into
`err_console.print`, which has Rich markup enabled. Every caller passes
`str(exc)` from a `BundlerError`, and those messages embed untrusted data
-- including the command's own argument -- so a `[...]` in it was parsed
as a style tag.
Balanced tags were silently swallowed; an unbalanced closer raised
`MarkupError`, which replaced the error message with a traceback and left
the output completely empty. Three commands crashed on user input alone,
with no project state required:
specify bundle catalog add 'ssh://ex[/red]ample.com/c.json'
specify bundle catalog remove 'no[/red]such'
specify bundle update 'no[/red]such'
`bundle validate` had the same failure on both branches: its errors echo
`requires.speckit_version`, and its warnings echo component ids, which are
not charset-validated -- so a structurally *valid* manifest crashed on the
success path too.
Fixed centrally in `_fail`, plus the remaining raw interpolations: the
`validate` warning/error/success lines, the install overlap and plan
warnings, the install/update/remove/catalog-add confirmations, the
`catalog list` id/url, and the `bundle init` project path.
Regression tests cover the four crashing error paths (parametrized) and
both `validate` branches; all six fail without this change.
Assisted-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(bundle): escape markup in `bundle list` records and `bundle build` output path
Review follow-up: two raw interpolations the first sweep missed, both on
success paths rather than error paths.
`bundle_list` rendered `record.bundle_id`, `record.version` and
`record.installed_at` unescaped. `InstalledBundleRecord.from_dict` only
requires non-empty strings for the first two and applies no charset check to
any of them, so a records file that *loads cleanly* still crashed the command
that displays it — confirmed as
`MarkupError: closing tag '[/red]' at position 12 doesn't match any open tag`.
`bundle_build` echoed `result.artifact_path` twice in its success line.
Brackets are legal in a directory name, so a bracketed `--output` built the
artifact and then misreported it: the work is already on disk when the
markup is consumed, so the line names a path that does not exist.
Re-scanned every `{...}` interpolation in the module to confirm nothing else
remains: the rest are either `BundlerError` messages that funnel through the
already-escaped `_fail`, `_format_component` output escaped at its call site
(:293), ints, or hardcoded enum `.value`s.
Two regression tests. The list case uses the unbalanced-closer form that
raises outright. The build case deliberately uses `[bold]` instead: `/` is a
path separator on Windows, so `dist[/red]out` becomes the directory
`dist[\red]out` and the fixture stops testing what it claims — the
silent-swallow form keeps it portable while still asserting the reported path
matches what was written. Verified both fail against 1d2184d.
tests/contract/test_bundle_cli.py -> 42 passed. tests/contract
tests/integration tests/unit -> 364 passed, 6 skipped, 5 failed; the 5 are
the pre-existing `*_refuses_symlinked_*` tests needing symlink privileges on
Windows, unchanged from main. ruff check passes.
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): skip an unreadable restore source in `preset remove`
`_unregister_skills_in_dir` restores each preset-owned SKILL.md from a core
command template or an extension source. Both of those reads were bare
`read_text(encoding="utf-8")` calls, so a project-owned override in
`.specify/templates/commands/` that exists but cannot be read or decoded
raised a raw `UnicodeDecodeError`/`OSError` straight out of
`PresetManager.remove()`, which has no handler for it — `specify preset
remove` dies with a traceback.
Every other failure in this loop degrades with `continue`: an unsafe
registry name, a missing skill subdirectory, a foreign owner. Sibling reads
of the very same directory are already guarded — `_infer_legacy_skill_
provenance` and `_delete_agent_preset_skills` both wrap their SKILL.md read
in `except (OSError, UnicodeDecodeError): continue`, and the read inside
`_substitute_core_template` was just given the same boundary in #3961. The
two restore reads were the remaining gap.
`continue` is the right recovery here rather than falling through: the
`else` branch below removes the skill outright, so treating an unreadable
source as "no source" would delete a user's skill at exactly the moment its
replacement cannot be generated. Skipping leaves the skill in place and
keeps it out of the returned `mutated_names`, so callers don't record a
restore that never happened.
Two regression tests, one per exception arm: a non-UTF-8 core template, and
a mocked `PermissionError` so the `OSError` half is also covered under
privileged CI where permission bits aren't enforced. Both assert the skill
survives untouched and is not reported as mutated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(presets): warn when a skill keeps preset content after a failed restore
Review follow-up on two points.
Surface the skipped restore. Skipping is still the correct recovery — the
alternative branch deletes the skill — but it was silent, and it is a
partial removal: `remove()` goes on to delete the preset directory and the
registry entry, while this `SKILL.md` keeps the removed preset's content,
and leaving the name out of `mutated_names` also keeps it out of
reconciliation, so nothing retries it. Both arms now emit a warning naming
the skill, the unreadable source, and the exception, and pointing at the
re-run that refreshes it once the file is fixed. `warnings.warn` matches
how the surrounding code reports non-fatal degradation (the reconciliation
failures in `remove()`/`install_from_directory`, the unreadable core
template in `_substitute_core_template` from #3961).
Cover the extension arm. A skill backed by an installed extension never
reaches the core-template read, so the two branches can regress
independently and both prior tests exercised only the core one.
`test_unregister_skills_in_dir_unreadable_extension_source_skips` installs
an extension whose command file is non-UTF-8 and asserts the skill survives
byte-for-byte and is absent from `mutated_names`. Verified it raises the
raw `UnicodeDecodeError` against unpatched source. The two existing tests
now assert the warning via `pytest.warns` so dropping it fails the suite.
pytest tests/test_presets.py -> 583 passed, 2 skipped, 7 failed; the 7 are
the pre-existing Windows symlink tests that need elevation, unchanged from
main. ruff check passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Capture and display the exception message when reading preset-catalogs.yml
fails, instead of swallowing the error details. Matches the pattern used
in preset_catalog_add 54 lines earlier.