Commit Graph
1778 Commits
Author SHA1 Message Date
5a052d36d1 [extension] Add SpecJudge extension to community catalog (#4079)
* Add SpecJudge extension to community catalog

Add specjudge extension submitted by @JoaquinRuiz to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table

Closes #4068

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Revert unrelated formatting-only edits in community catalog

Restore the compact single-line `tools` array formatting for existing
extensions that were incidentally expanded, keeping the diff scoped to
the SpecJudge addition and the catalog `updated_at` bump.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)

Co-authored-by: Copilot App <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>
2026-08-13 12:52:47 -05:00
Ali jawwadandClaude Opus 5 bfabf4ce65 fix(bundler): read the authoritative default_integration field, not only its legacy aliases (#3880)
* 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>
2026-08-13 12:19:58 -05:00
NgoQuocViet2001 b66044ae8e fix(auth): treat exact host patterns literally (#4108)
Assisted-by: OpenAI Codex (model: GPT-5, autonomous)
2026-08-13 12:18:44 -05:00
54f8b2cdf0 feat: add Mistral Vibe integration with Claude parity (#4075)
* 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>
2026-08-13 11:56:24 -05:00
13c3c942fc [extension] Add spec-kit-atlas extension to community catalog (#4105)
* Add spec-kit-atlas extension to community catalog

Add atlas extension submitted by @ashbrener to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table

Closes #3993

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>

---------

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>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-13 10:39:13 -05:00
Manfred Riemandgithub-actions[bot] 47938c30d7 chore: release 0.16.3, begin 0.16.4.dev0 development (#4107)
* 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>
2026-08-13 10:37:26 -05:00
Quratulain-bilal 16f45774a5 fix: narrow bare except Exception in VS Code settings merge (#3844)
* 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.
2026-08-13 10:36:00 -05:00
Manfred RiemandCopilot App 229022943c feat(presets): list presets in resolution/precedence order (#4086) (#4104)
`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>
2026-08-13 10:00:16 -05:00
Manfred RiemandCopilot App e79fa25f3f Fix: scaffold self-contained namespaced preset commands (#4076) (#4082)
* 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
2026-08-13 09:04:39 -05:00
github-actions[bot]andCopilot 750ce47cb8 Update Cross-Platform Governance preset to v0.2.2 (#4080)
Update cross-platform-governance preset submitted by @hindermath to:
- presets/catalog.community.json (version, download_url, documentation, description, provides, tags, updated_at)
- docs/community/presets.md community presets table

Closes #4078

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>
2026-08-13 08:36:36 -05:00
Ali jawwadandClaude Opus 5 197dde6253 fix(bundler): treat a blank active integration as indeterminate in FR-019 (#3886)
`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>
2026-08-12 11:52:34 -05:00
Luca Botti f2583e675c Integrate Junie with dot-to-hyphen behavior and command formatting (#4073)
* 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
2026-08-12 11:36:12 -05:00
github-actions[bot]andCopilot 996b24c561 Update A11Y Governance preset to v0.4.3 (#4074)
Update a11y-governance preset submitted by @hindermath to:
- presets/catalog.community.json (version, download_url, documentation, description, templates count, tags)
- docs/community/presets.md community presets table

Closes #4064


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>
2026-08-12 11:34:40 -05:00
WOLIKIMCHENGandroot b77ca572ca Fix Alquimia argument hints after folded descriptions (#4063)
Co-authored-by: root <kinsonnee@gmail.com>
2026-08-12 11:13:02 -05:00
Quratulain-bilal c1bceb625c fix: use bounded read for bundle download HTTP responses (#3764)
* 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
2026-08-12 08:29:36 -05:00
github-actions[bot]andCopilot 7dd706880e Update iSAQB Architecture Governance preset to v0.2.2 (#4056)
Update isaqb-architecture-governance preset submitted by @hindermath:
- presets/catalog.community.json (version, download_url, documentation, description, templates count, tags, updated_at)
- docs/community/presets.md community presets table

Closes #4055

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>
2026-08-11 16:58:14 -05:00
chelsealong bd595cf838 fix(claude): make argument-hint injection fold-aware for long descriptions (#4045)
* 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.
2026-08-11 13:14:19 -05:00
github-actions[bot]andCopilot 85d3ed289d Add SpecKit Grill Me extension to community catalog (#4052)
Add grill extension submitted by @yoshi1220 to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table

Closes #4047

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>
2026-08-11 13:12:48 -05:00
github-actions[bot]andCopilot 5ec937e07a Update Architecture Governance preset to v0.5.2 (#4050)
Update architecture-governance preset submitted by @hindermath to:
- presets/catalog.community.json (version, download_url, documentation, description, provides, tags, updated_at)
- docs/community/presets.md community presets table

Closes #4042

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>
2026-08-11 11:17:33 -05:00
Manfred RiemandCopilot App 6461f09483 Remove auto-assign from catalog submission workflow (#4054)
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
2026-08-11 11:16:53 -05:00
WOLIKIMCHENGandroot fb427e47ef docs: clarify example spec guidance (#4048)
Co-authored-by: root <kinsonnee@gmail.com>
2026-08-11 11:11:30 -05:00
WOLIKIMCHENGandroot bd04776dcc Clarify custom checklist ownership and lifecycle (#4028)
* docs: clarify custom checklist lifecycle

* docs: clarify implement checklist marker ownership

---------

Co-authored-by: root <kinsonnee@gmail.com>
2026-08-11 11:09:56 -05:00
github-actions[bot]andCopilot c5258e680b Update Archive Extension to v1.2.2 (#4053)
Update archive extension submitted by @stn1slv:
- extensions/catalog.community.json (version, download_url, updated_at)
- docs/community/extensions.md community extensions table

Closes #4049

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>
2026-08-11 10:29:58 -05:00
github-actions[bot]andCopilot 9d15554c08 Update Security Governance preset to v0.6.2 (#4040)
Update security-governance preset submitted by @hindermath to:
- presets/catalog.community.json (version, download_url, description, documentation, provides, tags, updated_at)
- docs/community/presets.md community presets table

Closes #4039

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>
2026-08-10 16:22:36 -05:00
Manfred RiemandCopilot App c3bbcc40a0 docs: clarify maintainer applies submission label during triage (#4041)
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
2026-08-10 15:37:51 -05:00
Manfred Riemandgithub-actions[bot] 39b56626c9 chore: release 0.16.2, begin 0.16.3.dev0 development (#4038)
* 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>
2026-08-10 14:46:12 -05:00
Ángel PeñaandCommandCodeBot 6aa9431b24 Add Command Code integration to spec-kit (#4019)
* 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>
2026-08-10 14:44:20 -05:00
Ali jawwadandClaude Opus 5 1b3695bfb3 fix(workflows): strip a resolved condition before the true/false check (#3883)
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>
2026-08-10 13:20:47 -05:00
Ali jawwadandClaude Opus 5 3451a21277 fix(workflows): guard a non-string overlay edit 'operation' (#3881)
`_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>
2026-08-10 13:19:50 -05:00
Quratulain-bilal fd8cc6da22 fix: bound response read in integration catalog fetch (#3818) 2026-08-10 13:18:36 -05:00
2df78f33fb Fix bug-test Python dependency provisioning (#4030)
* 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
2026-08-10 13:07:50 -05:00
Noor ul ainandClaude Opus 4.8 36da77f864 fix(bundle): escape Rich markup in bundle CLI error and status output (#4023)
* 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>
2026-08-10 12:34:02 -05:00
Noor ul ainandClaude Opus 4.8 1a44a6aa08 fix(presets): skip an unreadable restore source in preset remove (#4020)
* 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>
2026-08-10 12:26:07 -05:00
github-actions[bot]andCopilot f0ee1fc419 Add Keel Discovery extension to community catalog (#4035)
Add keel extension submitted by @athulrajeev to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table

Closes #4026

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>
2026-08-10 12:23:10 -05:00
Quratulain-bilal 44c3dc7d3e fix: show error details in preset catalog config read failure (#3840)
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.
2026-08-10 12:19:36 -05:00
github-actions[bot]andCopilot 2100f59f7e Update Reconcile Extension to v1.1.0 (#4034)
Update reconcile extension submitted by @stn1slv:
- extensions/catalog.community.json (version, download_url, updated_at)
- docs/community/extensions.md community extensions table

Closes #4024

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>
2026-08-10 12:13:19 -05:00
github-actions[bot]andCopilot a2c7ff29db Add Model Routing Governance preset to community catalog (#4033)
Add model-routing-governance preset submitted by @hindermath to:
- presets/catalog.community.json (alphabetical order)
- docs/community/presets.md community presets table

Closes #4021

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>
2026-08-10 12:11:20 -05:00
Quratulain-bilal ea1f5b8a2d fix: use missing_ok=True in integration JSON removal (#3846)
Replace check-then-act pattern (exists()+unlink()) with unlink(missing_ok=True)
to eliminate TOCTOU race condition.
2026-08-10 11:08:29 -05:00
Quratulain-bilal 29efcbe2c4 fix: use missing_ok=True in extension cache clear (#3845)
Replace check-then-act pattern (exists()+unlink()) with unlink(missing_ok=True)
to eliminate TOCTOU race condition. Matches the pattern already used for
per-URL cache files in the same method.
2026-08-10 11:07:11 -05:00
chelsealong 11e3176fd1 fix(extensions): reject duplicate provides.templates/scripts names (#4016)
The resolver returns the first entry matching a declared name, so a
later duplicate within provides.templates or provides.scripts was
silently unreachable while still counted by ExtensionManifest
properties. Reject duplicates at manifest-validation time instead.

Also clarify EXTENSION-DEVELOPMENT-GUIDE.md's provides section: hooks
and events are top-level manifest fields, not provides sub-fields, so
the "at least one of ..." wording doesn't imply they can be nested
under provides.
2026-08-10 10:23:23 -05:00
Manfred RiemandCopilot App 16cfab7724 feat(presets): resolve constitution templates at command time (#3984)
* feat(presets): resolve constitutions at command time

Gate install-time constitution materialization behind the constitution-sync preset while preserving one-time init seeding and authored-file safeguards.

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 7dbce70f-80c6-4e14-a30d-78cb358bcb84

* fix(presets): emit composed template content

Add a machine-readable preset resolve mode backed by PresetResolver.resolve_content and require the constitution command to consume it.

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 7dbce70f-80c6-4e14-a30d-78cb358bcb84

* fix(presets): unify runtime template composition

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 7dbce70f-80c6-4e14-a30d-78cb358bcb84

* fix(presets): secure runtime template resolution

Align runtime resolution across script variants, validate registry path components, and honor canonical extension ordering and convention paths.

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(presets): align runtime priority semantics

Normalize and tie-break preset priorities consistently across script variants, and preserve template bytes when Python materializes generated files.

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(presets): stop at effective template base

Avoid parsing irrelevant lower layers once resolution reaches a replace base, and decode raw bytes so Python preserves source line endings.

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(presets): align extension template resolution

Support root-level extension templates across runtime resolvers, fail safely when Bash cannot parse an extension registry, and validate requested templates in every prerequisite output mode.

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(presets): resolve dotted command identifiers

Route safe dotted names through command resolution, correct traversal coverage, and make Windows CI text decoding explicit.

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Avoid orphan feature directories on template errors

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* Align malformed preset manifest handling

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c

* Complete runtime resolver parity

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c

* Fail closed on resolver input errors

Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c

* Force UTF-8 and full manifest validation

Force UTF-8 decoding for registry and manifest reads in the Bash and
PowerShell embedded-Python parsers so resolution no longer depends on the
process locale, and validate every manifest template entry's required
fields, type, and strategy consistent with the canonical PresetManifest.

Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c

* Fail closed on empty manifests and corrupt registries

Reject manifests missing the provides/templates sections or declaring an
empty template list in all three runtime resolvers, matching the canonical
PresetManifest which treats those as invalid instead of silently degrading a
composing layer to a convention `replace` lookup.

Make a corrupt or unreadable extension registry fail closed in Bash,
PowerShell, and Python instead of swallowing the error and treating every
on-disk extension directory as unregistered-and-enabled, which could activate
a disabled extension.

Read the preset and extension registries as explicit UTF-8 in the PowerShell
resolver so priority/enabled-state decoding no longer depends on the process
code page under Windows PowerShell 5.1.

Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c

* fix(presets): fail closed when extension registry is not a regular file

The Bash and Python resolvers used is_file()/`-f` to gate reading the
extension `.registry`, which returns false for a directory or a broken
symlink at that path. In those cases the resolvers treated the registry
as absent and scanned every on-disk extension directory as unregistered
and enabled — a fail-open path. Detect any filesystem entry at the
registry path (including broken symlinks) and reject unless it is a
readable regular file. PowerShell now rejects a non-leaf entry explicitly
for parity. Adds directory- and broken-symlink parity regressions.

Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c

* fix(presets): fail closed on corrupt registry in canonical resolver and PowerShell

Two remaining fail-open paths for an invalid extension registry:

- The canonical PresetResolver enumerated extensions through
  ExtensionRegistry, whose _load() normalizes a corrupt or unreadable
  registry to an empty mapping. The directory scan then admitted every
  on-disk extension directory as unregistered-and-enabled, so a corrupt
  registry could still supply constitution content at init and through
  constitution-sync materialization. Add a non-invasive is_corrupt()
  probe (recovery behavior for install/enable/disable is unchanged) and
  raise from _get_all_extensions_by_priority() when the registry exists
  but is invalid. _load() now also recovers from OSError/UnicodeDecodeError
  so a directory or unreadable registry no longer crashes construction.

- The PowerShell resolver gated the registry read with Test-Path, which
  returns false for a dangling symlink on Windows, letting a broken
  .registry symlink bypass the guard and enable every on-disk extension.
  Detect the entry via directory enumeration (which observes a broken
  symlink) and reject it unless it is a readable regular file.

Adds canonical corrupt/directory-registry regressions and extends the
broken-symlink parity test to PowerShell.

Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c

* fix(presets): detect dangling registry symlink in ExtensionRegistry.is_corrupt

is_corrupt() gated on Path.exists(), which follows symlinks and returns
False for a dangling .registry symlink — so the canonical PresetResolver
treated it as an absent registry and fell back to scanning every on-disk
extension directory as unregistered-and-enabled, reopening the fail-open
path this guard closes. Detect lexical existence with os.path.lexists and
require a regular file before parsing, so a broken symlink (or directory)
is reported corrupt and resolution fails closed. Adds a canonical
broken-symlink regression alongside the directory case.

Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7dbce70f-80c6-4e14-a30d-78cb358bcb84
Copilot-Session: 3158e06f-95df-4e3a-843f-f159a35aa30c
2026-08-10 10:22:31 -05:00
1a60d1b6b8 [bug-fix] Fix preset-wrap-drops-argument-hint: inherit argument-hint from core template (#3996)
* Fix preset-wrap-drops-argument-hint: inherit argument-hint from core

Apply the remediation from the bug assessment on issue #3991.
Extend the inheritance allowlist in _register_skills and _compose_layers
to include 'argument-hint', so wrap-strategy presets that omit this key
will inherit it from the core template rather than silently dropping it
and risking its value being leaked into description.

Refs #3991

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(presets): guard wrap argument-hint inheritance for unmapped command

The existing regression test for #3991 wraps `speckit.specify`, whose stem
is in Claude's ARGUMENT_HINTS map. The string-injection fallback in
post_process_skill_content re-adds argument-hint even when wrap composition
drops it, so that test passes with or without the inheritance fix and does
not actually guard the regression.

Add a parallel test that wraps an extension-like command
(`speckit.myfeature`) absent from ARGUMENT_HINTS, so the wrap-composition
inheritance is the only path that can carry argument-hint into the SKILL.md.
This test fails without the fix and passes with it.

Refs #3991

Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 970babe2-48cd-4c41-adae-0282d879a9ce

---------

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>
Copilot-Session: 970babe2-48cd-4c41-adae-0282d879a9ce
2026-08-10 10:19:09 -05:00
d751231468 docs: document installing specify-cli from a custom package index (#4032)
* docs: document installing specify-cli from a custom package index

Add a generic section to the PyPI install guide covering how to point
uv, pipx, and pip at a non-default package index (env var and flags),
with a placeholder URL, plus notes on pins/upgrades and authentication.
Link to it from the main installation guide.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d6ef3f75-54e9-4789-902b-4f0adeaadfad

* 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>
Copilot-Session: d6ef3f75-54e9-4789-902b-4f0adeaadfad
2026-08-10 08:15:14 -05:00
chelsealong 684b3d8e05 feat(extensions): accept provides.templates and provides.scripts in manifest (#4012)
* feat(extensions): accept provides.templates and provides.scripts in manifest

Extensions could only formally declare commands under `provides`
(plus config/hooks/events); templates and scripts shipped by an
extension were picked up purely by filename convention, with no id,
description, or metadata. Add optional `provides.templates` and
`provides.scripts` sections to the extension manifest schema, mirroring
the preset template shape minus an authorable `strategy` (extension
artifacts always resolve as replace, so a present `strategy` key is
now a validation error rather than a silently accepted no-op).

ExtensionManifest gains `templates`/`scripts` properties so tooling can
enumerate an extension's declared artifacts directly from the
manifest. An extension may now satisfy the "must provide something"
rule with only a template or script, not just a command/hook/event.

Addresses the manifest-schema portion of #4010; resolver
authoritative-vs-convention precedence for these new sections is left
for a follow-up.

* fix(presets): wire extension-declared templates/scripts into resolver

collect_all_layers only consulted ExtensionManifest for command
resolution, leaving provides.templates/.scripts purely decorative --
a declared entry whose file didn't sit at the conventional path was
validated but never resolved. Extend the existing manifest-fallback
branch to cover template_type "template" and "script" the same way
it already does "command": convention lookup first, manifest lookup
as fallback so undeclared on-disk files keep resolving unchanged.

* fix(presets): make extension manifest lookup authoritative over convention

Copilot review on #4012 found the manifest-declared template/script lookup
was gated on convention lookup missing first, so a stale conventional file
could shadow a declared entry at a non-conventional path, and resolve()
never consulted the manifest at all (only collect_all_layers() did). Add a
shared _extension_manifest_declared_template() helper and check it before
convention-based lookup in both resolve() and collect_all_layers(), mirroring
the preset manifest precedence. Also update EXTENSION-DEVELOPMENT-GUIDE.md,
which still claimed provides only supports commands and required a command
or hook.

* fix(presets): stop resolving symlinks in extension manifest candidate path

_extension_manifest_declared_template() resolved ext_dir/rel_path before
returning it, which follows symlinks in ext_dir's ancestors (e.g. macOS's
symlinked tmp dir) and diverges from the unresolved paths convention-based
lookup returns for the same directory. Resolve only for the traversal
containment check; return the unresolved candidate.

Fixes the 4 CI test failures across all OS/Python matrix jobs on #4012.
2026-08-07 12:55:55 -05:00
Marsel SafinandCopilot 247abbf5e4 fix(presets): treat an unreadable core template as missing (#3961)
* fix(presets): treat an unreadable core template as missing

_substitute_core_template() read the resolved core template with a bare
read_text(), so one corrupted project-owned override in
.specify/templates/commands/ crashed the whole wrap-strategy command
registration with a raw UnicodeDecodeError. Both callers
(CommandRegistrar.register_pack and _register_commands) are unguarded
here, even though register_pack already skips an unreadable preset
source with a warning a few lines above the call.

Treat an unreadable core template like a missing one — warn and return
the body unchanged with empty frontmatter — matching the function's
documented no-core contract.

Assisted-by: GitHub Copilot (model: claude-fable-5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: assert the unreadable-core warning instead of suppressing it

Review follow-up: use pytest.warns so removing or changing the promised
warning fails the test.

Assisted-by: GitHub Copilot (model: claude-fable-5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-07 12:49:58 -05:00
Manfred Riemandgithub-actions[bot] 6da6956c3e chore: release 0.16.1, begin 0.16.2.dev0 development (#4014)
* chore: bump version to 0.16.1

* chore: begin 0.16.2.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-07 12:31:44 -05:00
Noor ul ainandClaude Opus 4.8 2a28f62e25 fix(integrations): wrap a non-UTF-8 catalog response (#4011)
* fix(integrations): wrap a non-UTF-8 catalog response

`_fetch_single_catalog` decodes the response body with `.decode("utf-8")`
before handing it to `json.loads`. A non-UTF-8 body therefore raises
`UnicodeDecodeError`, which is a sibling of `json.JSONDecodeError` under
`ValueError` rather than a subclass of it, so neither the `URLError` nor the
`JSONDecodeError` handler catches it.

The raw exception escapes `_get_merged_integrations`, whose
`except IntegrationCatalogError` is specifically designed to warn and skip a
bad catalog and carry on with the remaining ones. One catalog served over a
misconfigured proxy or truncated mid-multibyte-sequence thus takes down
`specify integration search` entirely instead of degrading to a warning.

Wrap it in `IntegrationCatalogError`, matching the convention already used
for the same decode in `authentication/azure_devops.py`, which lists
`UnicodeDecodeError` alongside `JSONDecodeError`.

Note that the cache-read path in this same method already tolerates this via
its `UnicodeError` clause; only the network path was unguarded.

Two regression tests: one pins the wrapped-error contract on the fetch, and
one covers the behaviour that actually motivates it — a broken catalog is
skipped with a warning while a healthy sibling catalog still resolves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(integrations): use the shared urlopen routing fixture

The raw-bytes helper patched `open_url` wholesale, which skipped the real
URL validation and redirect handling inside it. This module already imports
`route_opener_open_through_urlopen`, the repo's shared fixture that routes
`build_opener().open()` back through `urlopen` for exactly this reason, so
patching `urlopen` instead keeps the stub effective while still exercising
`open_url` itself.

Renamed to `_patch_urlopen_bytes` to sit alongside the existing
`_patch_urlopen`, whose signature it now mirrors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(integrations): restore the non-UTF-8 handler

The previous commit reverted the source change by accident while reworking
the tests, leaving the regression tests passing against an unfixed module.
Restores the `except UnicodeDecodeError` clause.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-07 12:17:46 -05:00
Marsel SafinandCopilot 5d9ac6a3f5 fix(events): skip an unreadable command template (#3956)
* fix(events): skip an unreadable command template

_render_command_template() read the resolved template with a bare
read_text(), so a template file that exists but cannot be read or
decoded (permission error, non-UTF-8 bytes) crashed event dispatch with
a raw OSError/UnicodeDecodeError. Every sibling failure in this path
(missing template, unresolvable command) already returns None so the
dispatcher falls back cleanly.

Wrap the read and return None on OSError/UnicodeDecodeError, matching
the sibling contract.

Assisted-by: GitHub Copilot (model: claude-fable-5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: cover the OSError half of the unreadable-template boundary

Review follow-up: add a mocked PermissionError case so both promised
exception paths are protected under privileged CI.

Assisted-by: GitHub Copilot (model: claude-fable-5, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-07 12:16:09 -05:00
Noor ul ainandClaude Opus 4.8 920ed7546d fix(bundle): wrap malformed YAML in a local .zip bundle manifest (#4013)
`_local_manifest_source` handles three local bundle sources. The directory
and `bundle.yml` branches both go through `BundleManifest.from_file` ->
`load_yaml`, which converts a parse failure into a `BundlerError`. The `.zip`
branch instead parses inline with a bare `_yaml.safe_load`.

`yaml.YAMLError` derives directly from `Exception` -- it is neither a
`ValueError` nor an `OSError` -- so it escapes `bundle_install`'s
`except BundlerError` and reaches the user as a raw
`yaml.parser.ParserError` traceback.

The remote counterpart of this same call, `_download_manifest`, already
guards it and even names `_yaml.YAMLError` explicitly. Only the local zip
path was missed, so the same corrupt manifest is reported cleanly when
fetched from a catalog but crashes when installed from disk.

Before, for the identical malformed bundle.yml:

    specify bundle install ./bundle-dir   ->  Error: Invalid YAML in ... (exit 1)
    specify bundle install ./bundle.yml   ->  Error: Invalid YAML in ... (exit 1)
    specify bundle install ./bundle.zip   ->  ParserError traceback

Two regression tests: one pins the `BundlerError` contract on the zip
branch, and one drives all three local sources through the CLI to assert
they now fail alike.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-07 12:04:39 -05:00
8865e57344 chore(deps): bump github/codeql-action/analyze from 4.37.3 to 4.37.5 (#4005)
* chore(deps): bump github/codeql-action/analyze from 4.37.3 to 4.37.5

Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.37.3 to 4.37.5.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81...d1ba80a13dd99fba24a470575428917156a28b43)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump github/codeql-action/init to match analyze 4.37.5

Dependabot only bumped the analyze step; keep init on the same
4.37.5 SHA so both CodeQL steps use the same release.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 126ceec5-7d64-444c-8cd4-d60b225d46f5

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@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>
Copilot-Session: 126ceec5-7d64-444c-8cd4-d60b225d46f5
2026-08-07 11:44:47 -05:00