Compare commits

...
23 Commits
Author SHA1 Message Date
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
bf88c9f9a8 Add SpecAssay bundle to community catalog (#4125)
Adds specassay v0.3.4 to bundles/catalog.community.json and
docs/community/bundles.md.

Validation results:
- Bundle ID matches ^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$: pass
- Version 0.3.4 is valid semver X.Y.Z: pass
- Repository https://github.com/rdryfoos/specassay is a public GitHub repo
  containing bundle.yml, README.md, and LICENSE: pass
- bundle.yml fields match submission (id, name, version, role, author,
  license, speckit_version, provides 1 extension + 1 preset): pass
- README documents role, components, required catalogs, and install steps: pass
- Download URL is a valid HTTPS GitHub release asset under the submitted repo: pass
- Release v0.3.4 exists and specassay-0.3.4.zip is attached: pass
- Catalog entry fields match submission and manifest; verified=false: pass
- Tags are 2-5 lowercase strings: pass (5 tags)
- Required catalogs documented (extensions + presets); README includes
  catalog add commands and testing details confirm catalog registration: pass
- All checklist items checked: pass

Closes #4059
cc @rdryfoos

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-14 11:44:41 -05:00
Manfred RiemGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
76f4cbc91d chore: release 0.16.4, begin 0.16.5.dev0 development (#4124)
* chore: bump version to 0.16.4

* chore: begin 0.16.5.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-14 10:50:05 -05:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Copilot
71470cb551 Add SpecAssay preset to community catalog (#4123)
Add specassay preset submitted by @rdryfoos to:
- presets/catalog.community.json (alphabetical order)
- docs/community/presets.md community presets table

Closes #4058

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-14 10:34:06 -05:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Copilot
7e0db461ec Update Intake Authoring Governance preset to v0.3.1 (#4121)
Update intake-authoring-governance preset submitted by @hindermath:
- presets/catalog.community.json (version, download_url, documentation, description, templates count)
- docs/community/presets.md community presets table

Closes #4118

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-14 10:17:38 -05:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Copilot
4faecb8422 Update Superspec extension to v1.0.2 (#4120)
Update superspec extension submitted by @CrazyBaran:
- extensions/catalog.community.json (version, download_url, updated_at)
- docs/community/extensions.md community extensions table

Closes #4117

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-14 10:13:47 -05:00
0121cabd3e fix(taskstoissues): widen task-ID regex to match IDs longer than 3 digits (#4101)
* 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>
2026-08-14 09:50:45 -05:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Copilot
7f4f576829 Add Architecture Governance extension to community catalog (#4122)
Add arch-governance extension submitted by @ashbrener to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table

Closes #4084

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-14 09:38:50 -05:00
Ngo Quoc VietandGitHub d6e09a17c4 fix(workflows): validate non-string step types (#4111)
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)
2026-08-14 09:18:25 -05:00
672f812927 Harden community submission workflow output allowlists (#4103)
* 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>
2026-08-14 09:17:02 -05:00
dependabot[bot]GitHubCopilot Appdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Manfred Riem
ea26fc265f chore(deps): bump github/codeql-action (init + analyze) from 4.37.5 to 4.37.6 (#4114)
* chore(deps): bump github/codeql-action/analyze from 4.37.5 to 4.37.6

Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.37.5 to 4.37.6.
- [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/d1ba80a13dd99fba24a470575428917156a28b43...5595ccaf912efad79be6eef63a5619ff05969be3)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.6
  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 (v4.37.6)

Bump github/codeql-action/init to 5595cca (v4.37.6) so it matches the
analyze bump already in this PR. init and analyze must be pinned to the
same version; a mismatch fails CodeQL with "Loaded a configuration file
for version '4.37.5', but running version '4.37.6'". This subsumes #4115.

Also group github/codeql-action* in dependabot.yml so future bumps of
init and analyze arrive as a single PR and can't drift apart again.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6a01da34-7431-4ffc-84ee-e51ecf224334

---------

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: 6a01da34-7431-4ffc-84ee-e51ecf224334
2026-08-14 09:10:45 -05:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Copilot
83883a2eba Add SpecAssay Check extension to community catalog (#4113)
Add specassay-check extension submitted by @rdryfoos to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table

Closes #4057

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 15:36:51 -05:00
b485cd8c1f fix(integrations): dispatch goose commands via goose run (#2416) (#3781)
* 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>
2026-08-13 15:02:48 -05:00
2b36f0ce94 fix(powershell): stop Out-Null swallowing the AVAILABLE_DOCS status lines (#3891)
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>
2026-08-13 14:58:46 -05:00
Quratulain-bilalandGitHub c807e2450c fix: remove TOCTOU race in RunState.load (#3839)
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.
2026-08-13 14:52:56 -05:00
56aec8a936 fix: decode the zipped manifest as UTF-8 before parsing (#3958)
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>
2026-08-13 14:47:30 -05:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Copilot
7346819039 Update Agent Parity Governance preset to v0.4.2 (#4110)
Update agent-parity-governance preset submitted by @hindermath to:
- presets/catalog.community.json (version, download_url, documentation, description, templates count)
- docs/community/presets.md community presets table

Closes #4109

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 14:29:09 -05:00
Quratulain-bilalandGitHub 618d16e94b fix: log progress tracker refresh errors instead of silently swallowing (#3975)
* 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
2026-08-13 13:25:28 -05:00
github-actions[bot]GitHubCopilot Appgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Manfred Riem
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
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
NgoQuocViet2001andGitHub 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
github-actions[bot]GitHubCopilotCopilot Autofix powered by AIgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Manfred Riem
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 RiemGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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
35 changed files with 1492 additions and 86 deletions
+4
View File
@@ -8,6 +8,10 @@ updates:
- dependency-name: "github/gh-aw-actions/**"
- dependency-name: "github/gh-aw-actions" # Managed by gh aw compile. Version-locked to the gh-aw compiler; do not bump.
package-ecosystem: github-actions
groups:
codeql-action:
patterns:
- "github/codeql-action*"
schedule:
interval: weekly
version: 2
+5 -5
View File
@@ -1,4 +1,4 @@
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"687ea37b376b3b918331c13fce6cdbf5b9898bab8e514ca57b662b92b6d3cd2c","body_hash":"83b7e917f475d6ddf32f17e7da09dd4097a01dddbcbbf8eeec673912285de8b2","compiler_version":"v0.79.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}}
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"f9532e77722bfd32e8f626cbfbf6c5372ddcd9997963d965d53e8531b3e28a15","body_hash":"83b7e917f475d6ddf32f17e7da09dd4097a01dddbcbbf8eeec673912285de8b2","compiler_version":"v0.79.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c0338fef4749d08c21f8f975fb0e37efa17dda47","version":"v0.79.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]}
# This file was automatically generated by gh-aw (v0.79.8). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
@@ -511,9 +511,9 @@ jobs:
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_7b8091d12bfe1e7b_EOF'
{"add_comment":{"max":2},"add_labels":{"allowed":["extension-submission","validation-passed","validation-failed","needs-info"],"max":3},"create_pull_request":{"draft":true,"labels":["extension-submission","automated"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","CONTRIBUTING.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"blocked","title_prefix":"[extension] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_7b8091d12bfe1e7b_EOF
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_253299f841a5ea45_EOF'
{"add_comment":{"max":2},"add_labels":{"allowed":["extension-submission","validation-passed","validation-failed","needs-info"],"max":3},"create_pull_request":{"allowed_files":["extensions/catalog.community.json","docs/community/extensions.md"],"draft":true,"labels":["extension-submission","automated"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","CONTRIBUTING.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"blocked","title_prefix":"[extension] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_253299f841a5ea45_EOF
- name: Generate Safe Outputs Tools
env:
GH_AW_TOOLS_META_JSON: |
@@ -1705,7 +1705,7 @@ jobs:
GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_API_URL: ${{ github.api_url }}
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":2},\"add_labels\":{\"allowed\":[\"extension-submission\",\"validation-passed\",\"validation-failed\",\"needs-info\"],\"max\":3},\"create_pull_request\":{\"draft\":true,\"labels\":[\"extension-submission\",\"automated\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"CONTRIBUTING.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"blocked\",\"title_prefix\":\"[extension] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}"
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":2},\"add_labels\":{\"allowed\":[\"extension-submission\",\"validation-passed\",\"validation-failed\",\"needs-info\"],\"max\":3},\"create_pull_request\":{\"allowed_files\":[\"extensions/catalog.community.json\",\"docs/community/extensions.md\"],\"draft\":true,\"labels\":[\"extension-submission\",\"automated\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"CONTRIBUTING.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"blocked\",\"title_prefix\":\"[extension] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}"
GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }}
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
@@ -31,6 +31,9 @@ safe-outputs:
labels: [extension-submission, automated]
draft: true
max: 1
allowed-files:
- extensions/catalog.community.json
- docs/community/extensions.md
protected-files:
policy: blocked
exclude:
+5 -5
View File
@@ -1,4 +1,4 @@
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b4ba1db5fdec754fa825cc3160879924118bc454a781eed70ef6c90beab83a95","body_hash":"cb6c19088fa13da0a8320c174e8c14c4887d2c8a005a5cb2d2d2faa3f890de39","compiler_version":"v0.79.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}}
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"09fb89e95c57c7beeaa0c823fb35f38d5f9db898a419e59dd0a323e7a9209753","body_hash":"cb6c19088fa13da0a8320c174e8c14c4887d2c8a005a5cb2d2d2faa3f890de39","compiler_version":"v0.79.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c0338fef4749d08c21f8f975fb0e37efa17dda47","version":"v0.79.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]}
# This file was automatically generated by gh-aw (v0.79.8). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
@@ -511,9 +511,9 @@ jobs:
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_78499ff7c917c441_EOF'
{"add_comment":{"max":2},"add_labels":{"allowed":["preset-submission","validation-passed","validation-failed","needs-info"],"max":3},"create_pull_request":{"draft":true,"labels":["preset-submission","automated"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","CONTRIBUTING.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"blocked","title_prefix":"[preset] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_78499ff7c917c441_EOF
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_429ff69f52872d0b_EOF'
{"add_comment":{"max":2},"add_labels":{"allowed":["preset-submission","validation-passed","validation-failed","needs-info"],"max":3},"create_pull_request":{"allowed_files":["presets/catalog.community.json","docs/community/presets.md"],"draft":true,"labels":["preset-submission","automated"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","CONTRIBUTING.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"blocked","title_prefix":"[preset] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}}
GH_AW_SAFE_OUTPUTS_CONFIG_429ff69f52872d0b_EOF
- name: Generate Safe Outputs Tools
env:
GH_AW_TOOLS_META_JSON: |
@@ -1705,7 +1705,7 @@ jobs:
GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_API_URL: ${{ github.api_url }}
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":2},\"add_labels\":{\"allowed\":[\"preset-submission\",\"validation-passed\",\"validation-failed\",\"needs-info\"],\"max\":3},\"create_pull_request\":{\"draft\":true,\"labels\":[\"preset-submission\",\"automated\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"CONTRIBUTING.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"blocked\",\"title_prefix\":\"[preset] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}"
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":2},\"add_labels\":{\"allowed\":[\"preset-submission\",\"validation-passed\",\"validation-failed\",\"needs-info\"],\"max\":3},\"create_pull_request\":{\"allowed_files\":[\"presets/catalog.community.json\",\"docs/community/presets.md\"],\"draft\":true,\"labels\":[\"preset-submission\",\"automated\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"CONTRIBUTING.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"blocked\",\"title_prefix\":\"[preset] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}"
GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }}
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
@@ -31,6 +31,9 @@ safe-outputs:
labels: [preset-submission, automated]
draft: true
max: 1
allowed-files:
- presets/catalog.community.json
- docs/community/presets.md
protected-files:
policy: blocked
exclude:
+2 -2
View File
@@ -22,11 +22,11 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Initialize CodeQL
uses: github/codeql-action/init@d1ba80a13dd99fba24a470575428917156a28b43 # v4
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
with:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@d1ba80a13dd99fba24a470575428917156a28b43 # v4
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
with:
category: "/language:${{ matrix.language }}"
+51
View File
@@ -2,6 +2,57 @@
<!-- insert new changelog below this comment -->
## [0.16.4] - 2026-08-14
### Changed
- Add SpecAssay preset to community catalog (#4123)
- Update Intake Authoring Governance preset to v0.3.1 (#4121)
- Update Superspec extension to v1.0.2 (#4120)
- fix(taskstoissues): widen task-ID regex to match IDs longer than 3 digits (#4101)
- Add Architecture Governance extension to community catalog (#4122)
- fix(workflows): validate non-string step types (#4111)
- Harden community submission workflow output allowlists (#4103)
- chore(deps): bump github/codeql-action (init + analyze) from 4.37.5 to 4.37.6 (#4114)
- Add SpecAssay Check extension to community catalog (#4113)
- fix(integrations): dispatch goose commands via `goose run` (#2416) (#3781)
- fix(powershell): stop Out-Null swallowing the AVAILABLE_DOCS status lines (#3891)
- fix: remove TOCTOU race in RunState.load (#3839)
- fix: decode the zipped manifest as UTF-8 before parsing (#3958)
- Update Agent Parity Governance preset to v0.4.2 (#4110)
- fix: log progress tracker refresh errors instead of silently swallowing (#3975)
- [extension] Add SpecJudge extension to community catalog (#4079)
- fix(bundler): read the authoritative `default_integration` field, not only its legacy aliases (#3880)
- fix(auth): treat exact host patterns literally (#4108)
- feat: add Mistral Vibe integration with Claude parity (#4075)
- [extension] Add spec-kit-atlas extension to community catalog (#4105)
- chore: release 0.16.3, begin 0.16.4.dev0 development (#4107)
## [0.16.3] - 2026-08-13
### Changed
- fix: narrow bare except Exception in VS Code settings merge (#3844)
- feat(presets): list presets in resolution/precedence order (#4086) (#4104)
- Fix: scaffold self-contained namespaced preset commands (#4076) (#4082)
- Update Cross-Platform Governance preset to v0.2.2 (#4080)
- fix(bundler): treat a blank active integration as indeterminate in FR-019 (#3886)
- Integrate Junie with dot-to-hyphen behavior and command formatting (#4073)
- Update A11Y Governance preset to v0.4.3 (#4074)
- Fix Alquimia argument hints after folded descriptions (#4063)
- fix: use bounded read for bundle download HTTP responses (#3764)
- Update iSAQB Architecture Governance preset to v0.2.2 (#4056)
- fix(claude): make argument-hint injection fold-aware for long descriptions (#4045)
- Add SpecKit Grill Me extension to community catalog (#4052)
- Update Architecture Governance preset to v0.5.2 (#4050)
- Remove auto-assign from catalog submission workflow (#4054)
- docs: clarify example spec guidance (#4048)
- Clarify custom checklist ownership and lifecycle (#4028)
- Update Archive Extension to v1.2.2 (#4053)
- Update Security Governance preset to v0.6.2 (#4040)
- docs: clarify maintainer applies submission label during triage (#4041)
- chore: release 0.16.2, begin 0.16.3.dev0 development (#4038)
## [0.16.2] - 2026-08-10
### Changed
+23 -1
View File
@@ -1,6 +1,6 @@
{
"schema_version": "1.0",
"updated_at": "2026-07-22T00:00:00Z",
"updated_at": "2026-08-14T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/bundles/catalog.community.json",
"bundles": {
"sicario-spec": {
@@ -30,6 +30,28 @@
"threat-modeling"
],
"verified": false
},
"specassay": {
"name": "SpecAssay",
"id": "specassay",
"version": "0.3.4",
"role": "developer",
"description": "Durable-ID promotion for stock Spec Kit: templates, Gate 2 refusal, and trace-manifest emission.",
"author": "Rik Dryfoos",
"license": "MIT",
"download_url": "https://github.com/rdryfoos/specassay/releases/download/v0.3.4/specassay-0.3.4.zip",
"repository": "https://github.com/rdryfoos/specassay",
"requires": {
"speckit_version": ">=0.14.0"
},
"provides": {
"extensions": 1,
"presets": 1,
"steps": 0,
"workflows": 0
},
"tags": ["traceability", "governance", "durable-ids", "gate", "sdd"],
"verified": false
}
}
}
+1
View File
@@ -10,6 +10,7 @@ Accepted community bundle entries are published in [`bundles/catalog.community.j
| Bundle | Purpose | Role or team | Provides | Required catalogs | URL |
|--------|---------|--------------|----------|-------------------|-----|
| SicarioSpec Security & Governance Bundle | Secure-by-default governance bundle for GitHub Spec Kit. Enforces data classification, threat modeling, and code-owned verification gates. | `security-engineer` | 1 extension, 11 presets | Documented | [sicario-spec](https://github.com/dfirs1car1o/sicario-spec) |
| SpecAssay | Durable-ID promotion for stock Spec Kit: templates, Gate 2 refusal, and trace-manifest emission. | `developer` | 1 extension, 1 preset | Documented | [specassay](https://github.com/rdryfoos/specassay) |
## What to Submit
+4
View File
@@ -32,9 +32,11 @@ The following community-contributed extensions are available in [`catalog.commun
| Analytics | Measure what your AI builds, and how much time it saves you | `visibility` | Read+Write | [spec-kit-analytics](https://github.com/Fyloss/spec-kit-analytics) |
| API Evolve | Managed API contract evolution — breaking-change detection, semver enforcement, deprecation orchestration, and lifecycle gates across REST, GraphQL, and gRPC | `process` | Read+Write | [spec-kit-api-evolve](https://github.com/Quratulain-bilal/spec-kit-api-evolve) |
| Architect Impact Previewer | Predicts architectural impact, complexity, and risks of proposed changes before implementation. | `visibility` | Read-only | [spec-kit-architect-preview](https://github.com/UmmeHabiba1312/spec-kit-architect-preview) |
| Architecture Governance | Keep specs, code & ADRs in sync: citation slots + a read-only, fail-closed validator | `docs` | Read+Write | [spec-kit-arch-governance](https://github.com/ashbrener/spec-kit-arch-governance) |
| Architecture Guard | Framework-agnostic architecture review extension for validating implementation against governance and architecture constitutions, detecting architectural drift, and generating non-blocking refactor tasks | `process` | Read+Write | [spec-kit-architecture-guard](https://github.com/DyanGalih/spec-kit-architecture-guard) |
| Architecture Workflow | Generate or reverse project-level 4+1 architecture views with per-view and full-workflow commands | `docs` | Read+Write | [spec-kit-arch](https://github.com/bigsmartben/spec-kit-arch) |
| Archive Extension | Archive merged features into main project memory, resolving gaps and conflicts. | `docs` | Read+Write | [spec-kit-archive](https://github.com/stn1slv/spec-kit-archive) |
| spec-kit-atlas | Synthesize spec-kit specs into faithful, interactive architecture storybooks & doc portals. | `docs` | Read-only | [spec-kit-atlas](https://github.com/ashbrener/spec-kit-atlas) |
| Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | `integration` | Read+Write | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) |
| Blueprint | Stay code-literate in AI-driven development: review a complete code blueprint for every task from spec artifacts before /speckit.implement runs | `docs` | Read+Write | [spec-kit-blueprint](https://github.com/chordpli/spec-kit-blueprint) |
| Blueprint Index — Living Architecture Map | A living architecture map for spec-driven projects, kept honest by a deterministic, low-friction, machine-first CI gate (JSON, self-healable) that blocks only when the map contradicts the specs or code. Brownfield or greenfield. | `process` | Read+Write | [spec-kit-blueprint](https://github.com/ogil109/spec-kit-blueprint) |
@@ -145,6 +147,8 @@ The following community-contributed extensions are available in [`catalog.commun
| Spec Validate | Comprehension validation, review gating, and approval state for spec-kit artifacts — staged quizzes, peer review SLA, and a hard gate before /speckit.implement | `process` | Read+Write | [spec-kit-spec-validate](https://github.com/aeltayeb/spec-kit-spec-validate) |
| Spec-Kit BDD | ATDD/BDD extension: convert specs to Gherkin scenarios, scaffold step definitions, and verify acceptance test coverage | `process` | Read+Write | [spec-kit-bdd](https://github.com/RSginer/spec-kit-bdd) |
| Spec2Cloud | Spec-driven workflow tuned for shipping to Azure | `process` | Read+Write | [spec2cloud](https://github.com/Azure-Samples/Spec2Cloud) |
| SpecAssay Check | Gate 2 refuses silent gaps and emits a trace-manifest (trace-manifest.json). | `visibility` | Read+Write | [specassay](https://github.com/rdryfoos/specassay) |
| SpecJudge — right-size the model before you implement | Recommends the model that fits your tasks, citing the spec fragment behind every level. | `process` | Read-only | [SpecJudge](https://github.com/JoaquinRuiz/SpecJudge) |
| SpecKit Companion | Live spec-driven progress — lifecycle capture, status, resume, and a turbo pipeline profile | `visibility` | Read+Write | [speckit-companion](https://github.com/alfredoperez/speckit-companion) |
| SpecKit Grill Me | Exhaustively resolve specification ambiguities and decisions before planning | `process` | Read+Write | [speckit-grill-me](https://github.com/yoshi1220/speckit-grill-me) |
| SpecTest | Auto-generate test scaffolds from spec criteria, map coverage, and find untested requirements | `code` | Read+Write | [spec-kit-spectest](https://github.com/Quratulain-bilal/spec-kit-spectest) |
+3 -2
View File
@@ -8,7 +8,7 @@ The following community-contributed presets customize how Spec Kit behaves — o
| Preset | Purpose | Provides | Requires | URL |
|--------|---------|----------|----------|-----|
| A11Y Governance | Adds WCAG 2.2 AA, accessible status output, bilingual CEFR-B2 delivery, inclusive-content and didactic-comment governance, and provider-neutral model routing. | 11 templates, 3 commands | — | [spec-kit-preset-a11y-governance](https://github.com/hindermath/spec-kit-preset-a11y-governance) |
| Agent Parity Governance | Adds shared-guidance and generated-command parity, repository-fleet completion evidence, secret-free runner/status metadata, audit-ready Spec-Kit run evidence, and agent-neutral model-routing guidance across declared AI-agent surfaces. | 6 templates, 3 commands | — | [spec-kit-preset-agent-parity-governance](https://github.com/hindermath/spec-kit-preset-agent-parity-governance) |
| Agent Parity Governance | Adds shared-guidance parity, fleet-completion evidence, secret-free runner metadata, audit-ready Spec Kit evidence, and agent-neutral model routing across declared AI-agent surfaces. | 7 templates, 3 commands | — | [spec-kit-preset-agent-parity-governance](https://github.com/hindermath/spec-kit-preset-agent-parity-governance) |
| AIDE In-Place Migration | Adapts the AIDE extension workflow for in-place technology migrations (X → Y pattern) — adds migration objectives, verification gates, knowledge documents, and behavioral equivalence criteria | 2 templates, 8 commands | AIDE extension | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
| Architecture Governance | Adds secure architecture, STRIDE/CAPEC threat modeling, arc42/S-ADR guidance, Zero Trust, SAMM, BSI cloud assurance, audit evidence, and provider-neutral model routing. | 14 templates, 3 commands | — | [spec-kit-preset-architecture-governance](https://github.com/hindermath/spec-kit-preset-architecture-governance) |
| Autonomous Run Governance | Adds permission-bounded autonomous delivery, an optional intake-review gate, and preservation of the project's learner and accessibility contract. | 13 templates, 5 commands, 4 scripts | — | [spec-kit-preset-autonomous-run-governance](https://github.com/hindermath/spec-kit-preset-autonomous-run-governance) |
@@ -19,7 +19,7 @@ The following community-contributed presets customize how Spec Kit behaves — o
| Explicit Task Dependencies | Adds explicit `(depends on T###)` dependency declarations and an Execution Wave DAG to tasks.md for parallel scheduling | 1 template, 1 command | — | [spec-kit-preset-explicit-task-dependencies](https://github.com/Quratulain-bilal/spec-kit-preset-explicit-task-dependencies) |
| Fiction Book Writing | It adapts the Spec-Driven Development workflow for storytelling to create books or audiobooks (with annotations) in 12 languages: features become story elements, specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports single and multi-POV, all major plot structure frameworks, and two style modes: an author voice sample or humanized AI prose principles. Supports interactive elements like brainstorming, interview, roleplay, and extras like statistics, cover builder, illustration builder, and bio command. Export with templates for KDP, D2D, etc. | 26 templates, 34 commands, 2 scripts | — | [speckit-preset-fiction-book-writing](https://github.com/adaumann/speckit-preset-fiction-book-writing) |
| Game Narrative Writing | Preset for game narrative design and interactive storytelling. It adapts the Spec-Driven Development workflow for game narratives: features become story mechanics, specs become narrative briefs, plans become story maps, and tasks become dialogue and scene-writing tasks. Supports branching narratives, player agency systems, state machines, and interactive dialogue trees. | 37 templates, 34 commands, 5 scripts | — | [speckit-preset-game-narrative-writing](https://github.com/adaumann/speckit-preset-game-narrative-writing) |
| Intake Authoring Governance | Governs traceable intake CRUD and language-aware requirements collections with atomic migrations, rollback evidence, and safe series authoring. | 12 templates, 5 commands, 7 scripts | — | [spec-kit-preset-intake-authoring-governance](https://github.com/hindermath/spec-kit-preset-intake-authoring-governance) |
| Intake Authoring Governance | Governs traceable intake CRUD, language-aware requirements collections, bounded public HTTPS sources, and explicitly approved single or series authoring. | 13 templates, 5 commands, 7 scripts | — | [spec-kit-preset-intake-authoring-governance](https://github.com/hindermath/spec-kit-preset-intake-authoring-governance) |
| Intake Review Governance | Reviews single, series, campaign, and language-aware requirements collections before Spec Kit execution. | 8 templates, 3 commands, 4 scripts | — | [spec-kit-preset-intake-review-governance](https://github.com/hindermath/spec-kit-preset-intake-review-governance) |
| Intake Sequencing Governance | Manages language-aware intake-series order, typed dependencies, lifecycle, and authority-neutral next-candidate selection. | 11 templates, 6 commands, 8 scripts | — | [spec-kit-preset-intake-sequencing-governance](https://github.com/hindermath/spec-kit-preset-intake-sequencing-governance) |
| iSAQB Architecture Governance | Adds iSAQB/CPSA-F and arc42 architecture governance, architecture views, quality scenarios, ADRs, risks, technical-debt evidence, and provider-neutral model routing. | 14 templates, 3 commands | — | [spec-kit-preset-isaqb-architecture-governance](https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance) |
@@ -33,6 +33,7 @@ The following community-contributed presets customize how Spec Kit behaves — o
| Security Governance | Adds memory-safe-language and secure-coding governance, exact-head security evidence, ASVS, supply-chain transparency, EU regulatory screening, and provider-neutral model routing. | 15 templates, 3 commands | — | [spec-kit-preset-security-governance](https://github.com/hindermath/spec-kit-preset-security-governance) |
| SicarioSpec Core | Baseline secure-by-default Spec Kit governance profile. | 5 templates | — | [sicario-spec](https://github.com/dfirs1car1o/sicario-spec) |
| Spec2Cloud | Spec-driven workflow tuned for shipping to Azure: spec → plan → tasks → implement → deploy | 5 templates, 8 commands | — | [spec2cloud](https://github.com/Azure-Samples/Spec2Cloud) |
| SpecAssay | Appends durable-ID, Carries, and SpecAssay vocabulary onto Spec Kit spec, tasks, and constitution templates. | 3 templates | — | [specassay](https://github.com/rdryfoos/specassay) |
| Table of Contents Navigation | Adds a navigable Table of Contents to generated spec.md, plan.md, and tasks.md documents | 3 templates, 3 commands | — | [spec-kit-preset-toc-navigation](https://github.com/Quratulain-bilal/spec-kit-preset-toc-navigation) |
| Test-First Governance | Governs TDD with coverage-complete BDD/ATDD Gherkin scenarios, explicit suite ownership, professional test reports, traceability, and risk-based quality gates. | 10 templates, 8 commands | — | [spec-kit-preset-test-first-governance](https://github.com/ka-zo/spec-kit-preset-test-first-governance) |
| VS Code Ask Questions | Enhances the clarify command to use `vscode/askQuestions` for batched interactive questioning. | 1 command | — | [spec-kit-presets](https://github.com/fdcastel/spec-kit-presets) |
+152 -4
View File
@@ -1,6 +1,6 @@
{
"schema_version": "1.0",
"updated_at": "2026-08-11T00:00:00Z",
"updated_at": "2026-08-14T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
"extensions": {
"adrkit": {
@@ -286,6 +286,44 @@
"created_at": "2026-05-14T00:00:00Z",
"updated_at": "2026-06-30T00:00:00Z"
},
"arch-governance": {
"name": "Architecture Governance",
"id": "arch-governance",
"description": "Keep specs, code & ADRs in sync: citation slots + a read-only, fail-closed validator.",
"author": "Ash Brener",
"version": "1.2.2",
"download_url": "https://github.com/ashbrener/spec-kit-arch-governance/archive/refs/tags/v1.2.2.zip",
"repository": "https://github.com/ashbrener/spec-kit-arch-governance",
"homepage": "https://github.com/ashbrener/spec-kit-arch-governance",
"documentation": "https://github.com/ashbrener/spec-kit-arch-governance/blob/main/README.md",
"changelog": "https://github.com/ashbrener/spec-kit-arch-governance/blob/main/CHANGELOG.md",
"license": "MIT",
"category": "docs",
"effect": "read-write",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{ "name": "python", "version": ">=3.11", "required": true },
{ "name": "uv", "required": true }
]
},
"provides": {
"commands": 6,
"hooks": 3
},
"tags": [
"architecture",
"governance",
"adr",
"citations",
"spec-sync"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-08-14T00:00:00Z",
"updated_at": "2026-08-14T00:00:00Z"
},
"architect-preview": {
"name": "Architect Impact Previewer",
"id": "architect-preview",
@@ -390,6 +428,44 @@
"created_at": "2026-03-14T00:00:00Z",
"updated_at": "2026-08-11T00:00:00Z"
},
"atlas": {
"name": "spec-kit-atlas",
"id": "atlas",
"description": "Synthesize spec-kit specs into faithful, interactive architecture storybooks & doc portals.",
"author": "Ash Brener",
"version": "0.1.0",
"download_url": "https://github.com/ashbrener/spec-kit-atlas/archive/refs/tags/v0.1.0.zip",
"repository": "https://github.com/ashbrener/spec-kit-atlas",
"homepage": "https://github.com/ashbrener/spec-kit-atlas",
"documentation": "https://github.com/ashbrener/spec-kit-atlas/blob/main/README.md",
"changelog": "https://github.com/ashbrener/spec-kit-atlas/blob/main/CHANGELOG.md",
"license": "MIT",
"category": "docs",
"effect": "read-only",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{ "name": "python", "version": ">=3.11", "required": true },
{ "name": "uv", "required": true }
]
},
"provides": {
"commands": 2,
"hooks": 0
},
"tags": [
"documentation",
"architecture",
"storybook",
"traceability",
"atlas"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-08-13T00:00:00Z",
"updated_at": "2026-08-13T00:00:00Z"
},
"azure-devops": {
"name": "Azure DevOps Integration",
"id": "azure-devops",
@@ -4148,6 +4224,78 @@
"created_at": "2026-04-30T00:00:00Z",
"updated_at": "2026-04-30T00:00:00Z"
},
"specassay-check": {
"name": "SpecAssay Check",
"id": "specassay-check",
"description": "Gate 2 refuses silent gaps and emits a trace-manifest (trace-manifest.json).",
"author": "Rik Dryfoos",
"version": "0.3.3",
"download_url": "https://github.com/rdryfoos/specassay/releases/download/v0.3.3/specassay-check-0.3.3.zip",
"repository": "https://github.com/rdryfoos/specassay",
"homepage": "https://www.specassay.com",
"documentation": "https://github.com/rdryfoos/specassay/blob/main/extensions/specassay-check/README.md",
"changelog": "https://github.com/rdryfoos/specassay/blob/main/CHANGELOG.md",
"license": "MIT",
"category": "visibility",
"effect": "read-write",
"requires": {
"speckit_version": ">=0.14.0",
"tools": [
{ "name": "bash", "required": true },
{ "name": "python3", "version": ">=3.8", "required": true }
]
},
"provides": {
"commands": 1,
"hooks": 1
},
"tags": [
"traceability",
"gate",
"ci",
"governance",
"sdd"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-08-13T00:00:00Z",
"updated_at": "2026-08-13T00:00:00Z"
},
"specjudge": {
"name": "SpecJudge — right-size the model before you implement",
"id": "specjudge",
"description": "Recommends the model that fits your tasks, citing the spec fragment behind every level.",
"author": "Joaquín Ruiz",
"version": "0.5.4",
"download_url": "https://github.com/JoaquinRuiz/SpecJudge/releases/download/v0.5.4/spec-kit-specjudge.zip",
"repository": "https://github.com/JoaquinRuiz/SpecJudge",
"homepage": "https://github.com/JoaquinRuiz/SpecJudge",
"documentation": "https://github.com/JoaquinRuiz/SpecJudge/blob/main/extensions/spec-kit/README.md",
"changelog": "https://github.com/JoaquinRuiz/SpecJudge/blob/main/extensions/spec-kit/CHANGELOG.md",
"license": "MIT",
"category": "process",
"effect": "read-only",
"requires": {
"speckit_version": ">=0.13.0",
"tools": [{ "name": "specjudge", "version": ">=0.5.0", "required": true }]
},
"provides": {
"commands": 1,
"hooks": 1
},
"tags": [
"cost",
"model-selection",
"local-first",
"ollama"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-08-12T00:00:00Z",
"updated_at": "2026-08-12T00:00:00Z"
},
"speckit-superpowers-bridge": {
"name": "Superpowers Implementation Bridge",
"id": "speckit-superpowers-bridge",
@@ -4456,8 +4604,8 @@
"id": "superspec",
"description": "Bridges spec-kit workflows with obra/superpowers capabilities for brainstorming, TDD, code review, and resumable execution.",
"author": "WangX0111",
"version": "1.0.1",
"download_url": "https://github.com/WangX0111/superspec/archive/refs/tags/v1.0.1.zip",
"version": "1.0.2",
"download_url": "https://github.com/WangX0111/superspec/archive/refs/tags/v1.0.2.zip",
"repository": "https://github.com/WangX0111/superspec",
"homepage": "https://github.com/WangX0111/superspec",
"documentation": "https://github.com/WangX0111/superspec/blob/main/README.md",
@@ -4484,7 +4632,7 @@
"downloads": 0,
"stars": 0,
"created_at": "2026-04-22T00:00:00Z",
"updated_at": "2026-05-30T00:00:00Z"
"updated_at": "2026-08-14T00:00:00Z"
},
"sync": {
"name": "Spec Sync",
+40 -13
View File
@@ -1,6 +1,6 @@
{
"schema_version": "1.0",
"updated_at": "2026-08-12T00:00:00Z",
"updated_at": "2026-08-14T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.community.json",
"presets": {
"a11y-governance": {
@@ -34,19 +34,19 @@
"agent-parity-governance": {
"name": "Agent Parity Governance",
"id": "agent-parity-governance",
"version": "0.4.1",
"description": "Adds shared-guidance and generated-command parity, repository-fleet completion evidence, secret-free runner/status metadata, audit-ready Spec-Kit run evidence, and agent-neutral model-routing guidance across declared AI-agent surfaces.",
"version": "0.4.2",
"description": "Adds shared-guidance parity, fleet-completion evidence, secret-free runner metadata, audit-ready Spec Kit evidence, and agent-neutral model routing across declared AI-agent surfaces.",
"author": "Thorsten Hindermann",
"repository": "https://github.com/hindermath/spec-kit-preset-agent-parity-governance",
"download_url": "https://github.com/hindermath/spec-kit-preset-agent-parity-governance/archive/refs/tags/v0.4.1.zip",
"download_url": "https://github.com/hindermath/spec-kit-preset-agent-parity-governance/archive/refs/tags/v0.4.2.zip",
"homepage": "https://github.com/hindermath/spec-kit-preset-agent-parity-governance",
"documentation": "https://github.com/hindermath/spec-kit-preset-agent-parity-governance/blob/v0.4.1/README.md",
"documentation": "https://github.com/hindermath/spec-kit-preset-agent-parity-governance/blob/v0.4.2/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.8.0"
},
"provides": {
"templates": 6,
"templates": 7,
"commands": 3
},
"tags": [
@@ -59,7 +59,7 @@
"multi-agent"
],
"created_at": "2026-04-27T00:00:00Z",
"updated_at": "2026-07-28T00:00:00Z"
"updated_at": "2026-08-13T00:00:00Z"
},
"aide-in-place": {
"name": "AIDE In-Place Migration",
@@ -347,19 +347,19 @@
"intake-authoring-governance": {
"name": "Intake Authoring Governance",
"id": "intake-authoring-governance",
"version": "0.3.0",
"description": "Governs traceable intake CRUD and language-aware requirements collections with atomic migrations, rollback evidence, and safe series authoring.",
"version": "0.3.1",
"description": "Governs traceable intake CRUD, language-aware requirements collections, bounded public HTTPS sources, and explicitly approved single or series authoring.",
"author": "Thorsten Hindermann",
"repository": "https://github.com/hindermath/spec-kit-preset-intake-authoring-governance",
"download_url": "https://github.com/hindermath/spec-kit-preset-intake-authoring-governance/archive/refs/tags/v0.3.0.zip",
"download_url": "https://github.com/hindermath/spec-kit-preset-intake-authoring-governance/archive/refs/tags/v0.3.1.zip",
"homepage": "https://github.com/hindermath/spec-kit-preset-intake-authoring-governance",
"documentation": "https://github.com/hindermath/spec-kit-preset-intake-authoring-governance/blob/v0.3.0/README.md",
"documentation": "https://github.com/hindermath/spec-kit-preset-intake-authoring-governance/blob/v0.3.1/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.8.3"
},
"provides": {
"templates": 12,
"templates": 13,
"commands": 5,
"scripts": 7
},
@@ -371,7 +371,7 @@
"migration"
],
"created_at": "2026-07-22T00:00:00Z",
"updated_at": "2026-07-28T00:00:00Z"
"updated_at": "2026-08-14T00:00:00Z"
},
"intake-review-governance": {
"name": "Intake Review Governance",
@@ -747,6 +747,33 @@
"created_at": "2026-04-30T00:00:00Z",
"updated_at": "2026-04-30T00:00:00Z"
},
"specassay": {
"name": "SpecAssay",
"id": "specassay",
"version": "0.3.4",
"description": "Appends durable-ID, Carries, and SpecAssay vocabulary onto Spec Kit spec, tasks, and constitution templates.",
"author": "Rik Dryfoos",
"repository": "https://github.com/rdryfoos/specassay",
"download_url": "https://github.com/rdryfoos/specassay/releases/download/v0.3.4/specassay-preset-0.3.4.zip",
"homepage": "https://github.com/rdryfoos/specassay",
"documentation": "https://github.com/rdryfoos/specassay/blob/main/presets/specassay/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.14.0"
},
"provides": {
"templates": 3,
"commands": 0
},
"tags": [
"traceability",
"durable-ids",
"governance",
"sdd"
],
"created_at": "2026-08-14T00:00:00Z",
"updated_at": "2026-08-14T00:00:00Z"
},
"test-first-governance": {
"name": "Test-First Governance",
"id": "test-first-governance",
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "specify-cli"
version = "0.16.3.dev0"
version = "0.16.5.dev0"
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
readme = "README.md"
requires-python = ">=3.11"
+11 -6
View File
@@ -157,13 +157,18 @@ if ($Json) {
Write-Output "FEATURE_DIR:$($paths.FEATURE_DIR)"
Write-Output "AVAILABLE_DOCS:"
# Show status of each potential document
Test-FileExists -Path $paths.RESEARCH -Description 'research.md' | Out-Null
Test-FileExists -Path $paths.DATA_MODEL -Description 'data-model.md' | Out-Null
Test-DirHasFiles -Path $paths.CONTRACTS_DIR -Description 'contracts/' | Out-Null
Test-FileExists -Path $paths.QUICKSTART -Description 'quickstart.md' | Out-Null
# Show status of each potential document.
# These helpers report their line with Write-Output and ALSO return a
# bool, both on the Success stream, so 'Out-Null' discarded the report
# line along with the return value and left AVAILABLE_DOCS empty. Drop
# only the boolean so the per-document lines reach stdout like the
# bash and Python twins.
Test-FileExists -Path $paths.RESEARCH -Description 'research.md' | Where-Object { $_ -isnot [bool] }
Test-FileExists -Path $paths.DATA_MODEL -Description 'data-model.md' | Where-Object { $_ -isnot [bool] }
Test-DirHasFiles -Path $paths.CONTRACTS_DIR -Description 'contracts/' | Where-Object { $_ -isnot [bool] }
Test-FileExists -Path $paths.QUICKSTART -Description 'quickstart.md' | Where-Object { $_ -isnot [bool] }
if ($IncludeTasks) {
Test-FileExists -Path $paths.TASKS -Description 'tasks.md' | Out-Null
Test-FileExists -Path $paths.TASKS -Description 'tasks.md' | Where-Object { $_ -isnot [bool] }
}
}
+4 -1
View File
@@ -7,6 +7,7 @@ layer, not out of it, to avoid circular imports.
"""
from __future__ import annotations
import logging
import sys
from collections.abc import Callable
@@ -21,6 +22,8 @@ from rich.text import Text
from rich.tree import Tree
from typer.core import TyperGroup
logger = logging.getLogger(__name__)
BANNER = """
███████╗██████╗ ███████╗ ██████╗██╗███████╗██╗ ██╗
██╔════╝██╔══██╗██╔════╝██╔════╝██║██╔════╝╚██╗ ██╔╝
@@ -85,7 +88,7 @@ class StepTracker:
try:
self._refresh_cb()
except Exception:
pass
logger.debug("Progress tracker refresh failed", exc_info=True)
def render(self):
tree = Tree(f"[cyan]{self.title}[/cyan]", guide_style="grey50")
+13 -6
View File
@@ -11,7 +11,6 @@ import json
import os
import stat
from dataclasses import dataclass
from fnmatch import fnmatch
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
@@ -48,10 +47,21 @@ def _is_valid_host_pattern(pattern: str) -> bool:
* ``*.example.com`` — leading ``*.`` wildcard; matches subdomains
such as ``myorg.example.com`` but not ``example.com`` itself
"""
if any(char in pattern for char in "?[]"):
return False
if "*" not in pattern:
return True # exact hostname — already validated as non-empty
# Only *.suffix is allowed; no other wildcard positions
return pattern.startswith("*.") and "*" not in pattern[2:]
return pattern.startswith("*.") and len(pattern) > 2 and "*" not in pattern[2:]
def _host_matches_pattern(hostname: str, pattern: str) -> bool:
"""Match a hostname against an exact host or leading ``*.`` wildcard."""
hostname = hostname.lower()
pattern = pattern.lower()
if pattern.startswith("*.") and _is_valid_host_pattern(pattern):
return hostname.endswith(pattern[1:])
return hostname == pattern
def _norm(value: Any) -> Any:
@@ -224,8 +234,5 @@ def find_entries_for_url(
return [
e
for e in entries
if any(
pattern == hostname or fnmatch(hostname, pattern)
for pattern in e.hosts
)
if any(_host_matches_pattern(hostname, pattern) for pattern in e.hosts)
]
+8 -4
View File
@@ -13,13 +13,18 @@ from __future__ import annotations
import urllib.error
import urllib.request
from fnmatch import fnmatch
from typing import Callable
from urllib.parse import urlparse
from .._download_security import is_safe_download_redirect
from . import get_provider
from .config import AuthConfigEntry, _default_config_path, find_entries_for_url, load_auth_config
from .config import (
AuthConfigEntry,
_default_config_path,
_host_matches_pattern,
find_entries_for_url,
load_auth_config,
)
_config_override: list[AuthConfigEntry] | None = None
@@ -54,8 +59,7 @@ def _load_config() -> list[AuthConfigEntry]:
def _hostname_in_hosts(hostname: str, hosts: tuple[str, ...]) -> bool:
"""Return True if *hostname* matches any pattern in *hosts*."""
hostname = hostname.lower()
return any(p == hostname or fnmatch(hostname, p) for p in hosts)
return any(_host_matches_pattern(hostname, pattern) for pattern in hosts)
RedirectValidator = Callable[[str, str], None]
+15 -1
View File
@@ -82,7 +82,21 @@ def active_integration(project_root: Path) -> str | None:
except BundlerError:
return None
if isinstance(data, dict):
value = data.get("integration") or data.get("id") or data.get("active")
# ``default_integration`` first, matching the canonical reader in
# ``integration_state`` (line 199):
# ``state.get("default_integration") or state.get("integration")``.
# ``write_integration_json`` writes both keys, so a marker produced by
# the current CLI already resolved through the ``integration`` alias --
# this is about which field is authoritative when they disagree, and
# about resolving a marker that carries only ``default_integration``
# (hand-edited, or written by anything that follows the canonical
# reader's shape). ``integration``/``id``/``active`` stay as fallbacks.
value = (
data.get("default_integration")
or data.get("integration")
or data.get("id")
or data.get("active")
)
if isinstance(value, str) and value:
return value
return None
+13 -3
View File
@@ -772,8 +772,6 @@ def _local_manifest_source(arg: str):
return BundleManifest.from_file(manifest_path)
if candidate.suffix == ".zip":
import io
import yaml as _yaml
from ..._download_security import open_zip_bounded, read_zip_member_limited
@@ -791,8 +789,20 @@ def _local_manifest_source(arg: str):
error_type=BundlerError,
label="bundle manifest",
)
# The bounded-zip helpers above keep archive failures inside the
# BundlerError contract, but the manifest bytes need the same
# treatment as yamlio.load_yaml: decode as UTF-8 explicitly —
# feeding PyYAML the byte stream would let its Reader auto-detect
# a UTF-16 BOM and accept a manifest the directory and bundle.yml
# sources reject.
try:
data = _yaml.safe_load(io.BytesIO(raw))
text = raw.decode("utf-8")
except UnicodeError as exc:
raise BundlerError(
f"Could not read bundle.yml inside '{candidate}': {exc}"
) from exc
try:
data = _yaml.safe_load(text)
except _yaml.YAMLError as exc:
# The sibling directory/bundle.yml branches reach YAML through
# load_yaml(), which turns a parse failure into a BundlerError. This
+171 -5
View File
@@ -297,6 +297,10 @@ def _emit(output, envelope, native_event=""):
hookSpecificOutput → {"hookSpecificOutput": {"hookEventName": ..., "additionalContext": ...}}
additionalContext → {"additionalContext": ...} (top-level, Copilot)
additional_context → {"additional_context": ...} (top-level, Cursor)
hook_specific_output → {"decision": "allow", "hook_specific_output":
{"additional_context": ...}} (Vibe: any non-empty
stdout must parse as a HookStructuredResponse or
the hook is reported failed and output dropped)
suppress → emit nothing (strict-JSON agents on events whose
output can't be used)
plain (default) → passthrough (Claude/Codex inject plain stdout)
@@ -320,6 +324,9 @@ def _emit(output, envelope, native_event=""):
if envelope == "additional_context":
sys.stdout.write(json.dumps({"additional_context": output}) + "\\n")
return
if envelope == "hook_specific_output":
sys.stdout.write(json.dumps({"decision": "allow", "hook_specific_output": {"additional_context": output}}) + "\\n")
return
sys.stdout.write(output)
@@ -339,9 +346,10 @@ def main():
timeout = 120
# Optional 5th arg: context-injection envelope for stdout (C13): plain
# (default), hookSpecificOutput, additionalContext, additional_context,
# or suppress. Unknown values fall back to plain passthrough.
# hook_specific_output, or suppress. Unknown values fall back to plain
# passthrough.
envelope = sys.argv[4] if len(sys.argv) >= 5 else "plain"
if envelope not in ("plain", "hookSpecificOutput", "additionalContext", "additional_context", "suppress"):
if envelope not in ("plain", "hookSpecificOutput", "additionalContext", "additional_context", "hook_specific_output", "suppress"):
envelope = "plain"
# Optional 6th arg: native event name for hookSpecificOutput's
# hookEventName field (required by Qwen's hooks spec; included by
@@ -672,8 +680,10 @@ def resolve_and_run_event_command(
context-injection protocol (C13): ``plain`` passthrough (Claude/Codex
inject plain stdout), ``hookSpecificOutput``/``additionalContext``/
``additional_context`` JSON wrappers (Gemini/Tabnine/Qwen/Devin, Copilot,
Cursor respectively), or ``suppress`` (strict-JSON agents on events whose
output can't be used).
Cursor respectively), ``hook_specific_output`` (Vibe's
HookStructuredResponse — any non-empty stdout that isn't valid JSON is
reported as a hook failure and dropped), or ``suppress`` (strict-JSON
agents on events whose output can't be used).
*native_event* is the agent's native hookEventName (e.g. ``"SessionStart"``),
required inside ``hookSpecificOutput`` by Qwen's hooks spec (and included
@@ -738,6 +748,13 @@ def _emit_event_stdout(output: str, envelope: str, native_event: str = "") -> No
if envelope == "additional_context":
sys.stdout.write(json.dumps({"additional_context": output}) + "\n")
return
if envelope == "hook_specific_output":
# Vibe parses any non-empty hook stdout as a HookStructuredResponse;
# plain text would be reported as a hook failure. Wrap it as an
# explicit allow with additional_context (injected on post_tool,
# harmlessly ignored on pre_tool/post_agent).
sys.stdout.write(json.dumps({"decision": "allow", "hook_specific_output": {"additional_context": output}}) + "\n")
return
sys.stdout.write(output)
@@ -1093,6 +1110,15 @@ def _shell_quote(value: str, target_os: str) -> str:
"""
if target_os == "windows":
return "'" + value.replace("'", "''") + "'"
if target_os == "cmd":
# cmd.exe (Vibe launches hooks via create_subprocess_shell, which is
# %COMSPEC% on Windows): single quotes are not quoting there, so a
# POSIX-quoted path with spaces would break apart. Double-quote only
# when needed; embedded double quotes are doubled (MSVCRT argv
# parsing treats "" inside a quoted string as a literal quote).
if re.fullmatch(r"[A-Za-z0-9_.\-\\/:]+", value):
return value
return '"' + value.replace('"', '""') + '"'
# "host" and "posix" both use POSIX quoting. On Windows the single-
# command-string formats (Claude/Gemini/Qwen/Devin/Tabnine) are run via
# Git Bash or the agent's POSIX-ish shell, so POSIX quoting is correct and
@@ -1100,6 +1126,17 @@ def _shell_quote(value: str, target_os: str) -> str:
return shlex.quote(value)
def _vibe_target_os() -> str:
"""Quoting target for Vibe hook commands.
Vibe launches hooks with ``asyncio.create_subprocess_shell`` — the host's
native shell: POSIX ``sh`` on Unix, ``cmd.exe`` (%COMSPEC%) on Windows,
where POSIX single-quoting is not quoting at all and an interpreter or
dispatcher path containing spaces would split.
"""
return "cmd" if os.name == "nt" else "host"
def _dispatcher_command(
integration: IntegrationBase,
project_root: Path,
@@ -1122,6 +1159,8 @@ def _dispatcher_command(
both POSIX and Windows variants into one checked-in file (Copilot): ``host``
uses the host-resolved interpreter (venv-aware), while ``posix``/``windows``
emit portable interpreters so the config works on either OS (#S4).
``cmd`` also uses the host-resolved interpreter but quotes for cmd.exe —
for agents that launch hooks through the native Windows shell (Vibe).
Each component is shell-quoted for the target shell (R2) so an interpreter
path with spaces or a command/event containing shell metacharacters is
@@ -1147,7 +1186,10 @@ def _dispatcher_command(
shape the agent's hook protocol requires. Plain-passthrough agents
(Claude/Codex) declare no envelope and get no extra argument.
"""
if target_os == "host":
if target_os in ("host", "cmd"):
# "cmd" is host-resolved too (venv-aware): it is selected only when
# generating on a Windows host for an agent that runs hooks through
# cmd.exe (Vibe), and differs from "host" purely in quoting style.
interpreter = _resolve_interpreter(project_root)
else:
interpreter = _resolve_interpreter_for_target(target_os)
@@ -1357,6 +1399,55 @@ def install_integration_events(
manifest.record_existing(rel)
created.append(config_path)
elif fmt == "toml-vibe":
# Vibe hooks.toml custom merge. Flat [[hooks]] array; Vibe's
# HookConfig schema is name/type/command/match/timeout, with type
# limited to "pre_tool" | "post_tool" | "post_agent". Hook names must
# be unique (Vibe silently drops duplicates by name), so a per-file
# counter suffix disambiguates handlers whose commands share a final
# segment (e.g. speckit.a.validate vs speckit.b.validate).
lines: list[str] = []
used_names: set[str] = set()
for ev, handlers in filtered.items():
native = canonical_to_native[ev]
for cfg in handlers:
command = cfg.get("command", "")
dispatcher_cmd = _dispatcher_command(
integration, project_root, command, ev,
target_os=_vibe_target_os(),
timeout_seconds=cfg.get("timeout", 60),
)
command_stem = command.split('.')[-1] if command else "unknown"
command_stem = re.sub(r'[^A-Za-z0-9_-]+', '-', command_stem) or "unknown"
base_name = f"speckit-{native}-{command_stem}"
hook_name = base_name
suffix = 2
while hook_name in used_names:
hook_name = f"{base_name}-{suffix}"
suffix += 1
used_names.add(hook_name)
lines.append("[[hooks]]")
lines.append(f'name = {_toml_quote(hook_name)}')
lines.append(f'type = {_toml_quote(native)}')
# Vibe's field is `match` (fnmatch glob, or `re:`-prefixed
# regex, case-insensitive) and it is only valid on tool
# hooks — HookConfig rejects `match` on post_agent. Canonical
# matchers are Claude-style regexes ("Edit|Write"), so
# non-wildcard matchers are emitted as `re:` patterns.
matcher = cfg.get("matcher", "*")
if matcher and matcher != "*" and native in ("pre_tool", "post_tool"):
lines.append(f'match = {_toml_quote("re:" + matcher)}')
lines.append(f'command = {_toml_quote(dispatcher_cmd)}')
lines.append(f'timeout = {_native_timeout(integration, cfg.get("timeout", 60) + EVENT_TIMEOUT_BUFFER)}')
lines.append('speckit_marker = true')
lines.append('')
# S5: only track when the merge wrote (skips on unreadable file).
if _merge_vibe_toml_fragment(config_path, "\n".join(lines)):
rel = str(config_path.relative_to(project_root))
if rel not in manifest.files:
manifest.record_existing(rel)
created.append(config_path)
elif fmt == "json-flat":
# Cursor hooks.json custom merge. Flat command-string entries, one
# per handler (#2), single resolved command string (#6/#16).
@@ -1479,6 +1570,8 @@ def _remove_native_event_hooks(
_remove_copilot_entries(config_path)
elif fmt == "toml":
_remove_toml_entries(config_path)
elif fmt == "toml-vibe":
_remove_vibe_toml_entries(config_path)
elif fmt in ("json-nested", "json-flat"):
_remove_json_entries(config_path)
elif fmt == "json-root-nested":
@@ -1973,6 +2066,42 @@ def _merge_toml_fragment(dst: Path, fragment: str) -> bool:
return True
def _merge_vibe_toml_fragment(dst: Path, fragment: str) -> bool:
"""Merge Specify-owned Vibe TOML hook entries into *dst*, regenerating the file.
Vibe uses a flat [[hooks]] array with type/matcher/command fields.
This removes any existing Specify-marked hooks and appends the new fragment.
An unreadable or undecodable pre-existing file aborts the merge instead
of discarding the user's bytes, mirroring ``_load_user_json`` (#22).
Returns False when skipped so callers avoid tracking the untouched file
(S5).
"""
_ensure_safe_destination(dst)
existing = ""
if dst.exists():
try:
existing = dst.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
logger.warning(
"Could not read %s (it may be unreadable or not UTF-8); "
"skipping event-config merge to preserve user content.",
dst,
)
logger.debug("Read error detail: %s", exc)
return False
# Remove existing Specify-marked [[hooks]] blocks
# Match [[hooks]] ... speckit_marker = true (with any content in between)
existing = re.sub(
r'\[\[hooks\]\]\n(?:(?!\[\[hooks\]\]).)*?speckit_marker = true\n*',
"",
existing,
flags=re.DOTALL,
)
dst.parent.mkdir(parents=True, exist_ok=True)
dst.write_text(existing.rstrip() + "\n\n" + fragment + "\n", encoding="utf-8")
return True
def _remove_toml_entries(dst: Path) -> bool:
"""Remove Specify-marked TOML entries; delete the file if now empty (#14).
@@ -2016,6 +2145,43 @@ def _remove_toml_entries(dst: Path) -> bool:
return False
def _remove_vibe_toml_entries(dst: Path) -> bool:
"""Remove Specify-marked Vibe TOML hook entries; delete the file if now empty.
Returns True if the file was deleted (no user content remained).
"""
if not dst.exists():
return False
_ensure_safe_destination(dst)
try:
existing = dst.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as exc:
logger.warning(
"Could not read %s (it may be unreadable or not UTF-8); "
"skipping event-config cleanup to preserve user content.",
dst,
)
logger.debug("Read error detail: %s", exc)
return False
# Remove Specify-marked [[hooks]] blocks
cleaned = re.sub(
r'\[\[hooks\]\]\n(?:(?!\[\[hooks\]\]).)*?speckit_marker = true\n*',
"",
existing,
flags=re.DOTALL,
)
# If only whitespace/comments remain, the file had no user content
stripped = "\n".join(
line for line in cleaned.splitlines()
if line.strip() and not line.strip().startswith("#")
)
if not stripped:
dst.unlink(missing_ok=True)
return True
dst.write_text(cleaned, encoding="utf-8")
return False
def _merge_copilot_json(dst: Path, new_hooks: dict[str, list]) -> bool:
"""Merge Specify-owned hooks into Copilot's dedicated hooks JSON (#8).
@@ -1,5 +1,7 @@
"""Goose integration — open source AI agent (Agentic AI Foundation)."""
from __future__ import annotations
from ..base import YamlIntegration
@@ -18,3 +20,84 @@ class GooseIntegration(YamlIntegration):
"args": "{{args}}",
"extension": ".yaml",
}
def build_exec_args(
self,
prompt: str,
*,
model: str | None = None,
output_json: bool = True,
) -> list[str] | None:
"""Build CLI arguments for non-interactive ``goose`` execution.
``YamlIntegration`` never overrode ``build_exec_args()``, so Goose
inherited the ``IntegrationBase`` no-op returning ``None``. Callers read
``None`` as "this CLI is unavailable", so a workflow command/prompt step
targeting Goose reported ``CLI not found or not installed`` even with
``goose`` on ``PATH`` (the Goose item in issue #2416).
``goose`` has no ``-p`` flag; its 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`` string
parameter, so a ``/speckit.<name> <rest>`` invocation maps onto
``--recipe <path> --params args=<rest>``. Only that namespace is
mapped: ``--recipe`` is a *path* Spec Kit synthesizes, unlike
opencode's ``--command`` or hermes' ``-s``, which hand a bare name to
the agent's own resolver. Any other prompt -- including Goose's own
session commands such as ``/help`` or ``/plan`` -- goes to ``-t``.
"""
args = [self._resolve_executable(), "run"]
# Extra args are applied first, matching the opencode / codex /
# cursor-agent ordering. Positional parity only, NOT precedence:
# ``goose run`` is clap-derive based, and --recipe / --model /
# --output-format are single-value args with no ``args_override_self``,
# so re-passing any of them through
# SPECKIT_INTEGRATION_GOOSE_EXTRA_ARGS makes goose exit with
# "cannot be used multiple times" whichever side comes first. The same
# is true of goose's boolean flags. Only its ``Vec``-typed args (which
# clap infers as ArgAction::Append) may legitimately repeat.
self._apply_extra_args_env_var(args)
if model:
args.extend(["--model", model])
if output_json:
args.extend(["--output-format", "json"])
# Only the ``speckit.`` namespace maps to a recipe: this branch
# synthesizes a *path*, and ``command_filename()`` can only ever spell
# ``speckit.<name>.yaml``. ``PromptStep`` passes arbitrary ``prompt:``
# strings here, so other slash text -- including Goose's own session
# commands ``/help`` and ``/plan`` -- must reach ``-t`` unchanged.
if prompt.startswith("/speckit."):
command, _, remainder = prompt[1:].partition(" ")
# ``command_filename`` re-adds the ``speckit.`` prefix and the
# ``.yaml`` extension, so strip it here; a dotted extension command
# (``speckit.git.commit``) round-trips too. A bare ``/speckit.``
# leaves no stem and falls through to ``-t``.
stem = command[len("speckit."):]
if stem:
# Derive the recipe path from the same two sources ``setup()``
# uses -- ``config["folder"]`` + ``config["commands_subdir"]``
# (exactly what ``commands_dest()`` does) and
# ``command_filename()`` -- so the dispatch target cannot drift
# from the file that was actually installed.
folder = (self.config.get("folder") or "").strip("/")
subdir = (self.config.get("commands_subdir") or "").strip("/")
# Relative, forward-slash path: dispatch runs with
# ``cwd=project_root``, and goose accepts a POSIX separator on
# every platform (``commands_dest()`` yields backslashes on
# win32).
parts = [
p for p in (folder, subdir, self.command_filename(stem)) if p
]
args.extend(["--recipe", "/".join(parts)])
if remainder.strip():
args.extend(["--params", f"args={remainder}"])
return args
args.extend(["-t", prompt])
return args
+96 -9
View File
@@ -11,9 +11,25 @@ from typing import Any
from ..base import IntegrationOption, SkillsIntegration
from ..manifest import IntegrationManifest
from ..._utils import dump_frontmatter
# Per-command frontmatter overrides for skills that should run in a forked
# subagent context.
#
# This is intentionally empty. ``analyze`` was previously forked (added in
# #2511) on the assumption that its heavy reads collapse to a short summary,
# but in practice ``/speckit-analyze`` returns a 300-500 line report that is
# injected back into the main conversation. In long sessions each subsequent
# fork inherits that growing context, compounding overhead until the chat
# freezes (#3185). Until a command genuinely returns a compact result, no
# command opts into ``context: fork``. The injection mechanism below stays in
# place so a future command can be added here when that holds true.
FORK_CONTEXT_COMMANDS: dict[str, dict[str, str]] = {}
class VibeIntegration(SkillsIntegration):
"""Integration for Mistral Vibe skills."""
key = "vibe"
config = {
"name": "Mistral Vibe",
@@ -28,24 +44,63 @@ class VibeIntegration(SkillsIntegration):
"args": "$ARGUMENTS",
"extension": "/SKILL.md",
}
multi_install_safe = True
# Vibe's hooks schema supports exactly three hook types (HookConfig
# rejects anything else): pre_tool, post_tool, post_agent. Unsupported
# canonical events (session_start/session_end/user_prompt_submit) are
# intentionally absent so install_integration_events skips them with a
# warning instead of writing entries Vibe would refuse to load.
CANONICAL_TO_NATIVE = {
"pre_tool_use": "pre_tool",
"post_tool_use": "post_tool",
"stop": "post_agent",
}
events_config_file = ".vibe/hooks.toml"
events_format = "toml-vibe"
# Vibe parses any non-empty hook stdout as a JSON HookStructuredResponse;
# plain text is reported as a hook failure and its output dropped. The
# dispatcher therefore wraps handler stdout as {"decision": "allow",
# "hook_specific_output": {"additional_context": ...}} for every event:
# post_tool injects additional_context, pre_tool/post_agent ignore it but
# still parse cleanly.
events_context_envelope = {"*": "hook_specific_output"}
@classmethod
def options(cls) -> list[IntegrationOption]:
return [
opts = super().options()
opts.append(
IntegrationOption(
"--skills",
is_flag=True,
default=True,
help="Install as agent skills",
),
]
)
return opts
def _render_skill(self, template_name: str, frontmatter: dict[str, Any], body: str) -> str:
"""Render a processed command template as a Vibe skill."""
skill_name = f"speckit-{template_name.replace('.', '-')}"
description = frontmatter.get(
"description",
f"Spec-kit workflow command: {template_name}",
)
skill_frontmatter = self._build_skill_fm(
skill_name, description, f"templates/commands/{template_name}.md"
)
frontmatter_text = dump_frontmatter(skill_frontmatter)
return f"---\n{frontmatter_text}\n---\n\n{body.strip()}\n"
def _build_skill_fm(self, name: str, description: str, source: str) -> dict:
from specify_cli.agents import CommandRegistrar
return CommandRegistrar.build_skill_frontmatter(
self.key, name, description, source
)
@staticmethod
def _inject_frontmatter_flag(content: str, key: str, value: str = "true") -> str:
"""
Insert ``key: value`` before the closing ``---`` if not already present.
Value: true by default
"""
"""Insert ``key: value`` before the closing ``---`` if not already present."""
lines = content.splitlines(keepends=True)
# Pre-scan: bail out if already present in frontmatter
@@ -80,13 +135,45 @@ class VibeIntegration(SkillsIntegration):
out.append(line)
return "".join(out)
def post_process_skill_content(self, content: str) -> str:
@staticmethod
def _skill_stem_from_content(content: str) -> str | None:
"""Derive the command stem (e.g. ``analyze``) from a skill's frontmatter.
Reads the ``name:`` field of the first frontmatter block and strips
the ``speckit-`` prefix. Returns ``None`` when no name is present.
"""
Inject shared hook guidance and Vibe-specific frontmatter flags:
- user-invocable: allows the skill to be invoked by the user (not just other agents)
dash_count = 0
for line in content.splitlines():
stripped = line.rstrip("\r\n")
if stripped == "---":
dash_count += 1
if dash_count == 2:
break
continue
if dash_count == 1 and stripped.startswith("name:"):
name = stripped[len("name:"):].strip().strip('"').strip("'")
if name.startswith("speckit-"):
return name[len("speckit-"):]
return name or None
return None
def post_process_skill_content(self, content: str) -> str:
"""Inject Vibe-specific frontmatter flags.
Applied by every skill-generation path (setup, presets, extensions),
so Vibe-specific frontmatter stays consistent however the SKILL.md
was produced.
"""
updated = super().post_process_skill_content(content)
updated = self._inject_frontmatter_flag(updated, "user-invocable")
updated = self._inject_frontmatter_flag(updated, "disable-model-invocation", "false")
stem = self._skill_stem_from_content(updated)
if stem:
fork_config = FORK_CONTEXT_COMMANDS.get(stem)
if fork_config:
for key, value in fork_config.items():
updated = self._inject_frontmatter_flag(updated, key, value)
return updated
def setup(
+16 -4
View File
@@ -366,6 +366,17 @@ def _validate_steps(
# Determine step type
step_type = step_config.get("type", "command")
if not isinstance(step_type, str):
# Registry keys are strings. Checking an unhashable YAML value
# (for example ``type: [shell]`` or a mapping) against the set
# below raises a raw TypeError before validation can report the
# authoring mistake. Guard every non-string shape first, matching
# the typed validation already applied to workflow and step IDs.
errors.append(
f"Step {step_id!r}: 'type' must be a string, got "
f"{type(step_type).__name__} ({step_type!r})."
)
continue
if step_type not in _get_valid_step_types():
errors.append(
f"Step {step_id!r} has invalid type {step_type!r}."
@@ -743,12 +754,13 @@ class RunState:
cls._validate_run_id(run_id)
runs_dir = project_root / ".specify" / "workflows" / "runs" / run_id
state_path = runs_dir / "state.json"
if not state_path.exists():
try:
with open(state_path, encoding="utf-8") as f:
state_data = json.load(f)
except FileNotFoundError:
msg = f"Run state not found: {state_path}"
raise FileNotFoundError(msg)
with open(state_path, encoding="utf-8") as f:
state_data = json.load(f)
if not isinstance(state_data, dict):
raise ValueError("Invalid run state: expected a JSON object")
missing_fields = [
+1 -1
View File
@@ -64,7 +64,7 @@ git config --get remote.origin.url
> [!CAUTION]
> ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL
1. **Fetch existing issues for deduplication**: Before creating anything, build the set of task IDs you are about to process from `tasks.md` (each is a `T` followed by three digits, e.g. `T001`). Then use the GitHub MCP server's `list_issues` tool to look for issues that already cover those IDs. Do not pass a `state` value, since omitting it makes the tool return both open and closed issues. Request `perPage: 100` to keep the number of calls down, and since the tool uses cursor-based pagination, request pages with the `after` parameter (using the `endCursor` from the previous response). For each issue title, match it against the task ID pattern `\bT\d{3}\b` (word boundaries so tokens like `ST001` or `T0010` are not matched by mistake; this also recognises titles written as `T001 ...`, `T001: ...` or `[T001] ...`) and, when it matches one of your task IDs, mark that ID as already having an issue. Stop paginating as soon as every task ID has been matched, or when there are no more pages, so you do not keep fetching the whole repository's issue history once all task IDs are accounted for. This bounds the number of calls on repos with large issue histories and still prevents duplicates when the command is re-run after `tasks.md` is regenerated or the skill is re-invoked.
1. **Fetch existing issues for deduplication**: Before creating anything, build the set of task IDs you are about to process from `tasks.md` (each is a `T` followed by **at least** three digits, e.g. `T001` — `__SPECKIT_COMMAND_CONVERGE__` assigns new IDs with `T{M+1:03d}`, which is a floor rather than a cap, so once a file has more than 999 tasks the IDs are four digits or longer). Then use the GitHub MCP server's `list_issues` tool to look for issues that already cover those IDs. Do not pass a `state` value, since omitting it makes the tool return both open and closed issues. Request `perPage: 100` to keep the number of calls down, and since the tool uses cursor-based pagination, request pages with the `after` parameter (using the `endCursor` from the previous response). For each issue title, match it against the task ID pattern `\bT\d{3,}\b` (the `{3,}` accepts four-digit and longer IDs — with `\d{3}` a title containing `T1000` would not match at all, because the trailing `\b` cannot fall between two digits, so that task would be silently neither deduplicated nor created; word boundaries still stop a token like `ST001` from matching, and force the whole digit run to be consumed so `T100` can never match inside `T1000`; this also recognises titles written as `T001 ...`, `T001: ...` or `[T001] ...`) and, when it matches one of your task IDs, mark that ID as already having an issue. Stop paginating as soon as every task ID has been matched, or when there are no more pages, so you do not keep fetching the whole repository's issue history once all task IDs are accounted for. This bounds the number of calls on repos with large issue histories and still prevents duplicates when the command is re-run after `tasks.md` is regenerated or the skill is re-invoked.
1. For each task in the list, use the GitHub MCP server to create a new issue in the repository that is representative of the Git remote. Task lines in `tasks.md` start with a markdown checkbox, so first strip the leading `- [ ]` (and any `[P]` / `[US#]` markers) to recover the task ID and its description. Create the issue with a single canonical title of the form `T001: <description>`, with the ID written once followed by the task description (for example, the line `- [ ] T001 Create project structure` becomes the title `T001: Create project structure`).
- **Skip** any task whose ID is already present in the set of existing issues from the previous step, and report it (for example, `T001 already has an issue, skipping`).
- Only create issues for tasks that do not yet have a matching issue.
@@ -62,6 +62,39 @@ def test_local_source_rejects_unknown_file(tmp_path: Path):
_local_manifest_source(str(weird))
def test_local_source_zip_non_utf8_manifest_raises_bundler_error(tmp_path: Path):
"""Undecodable bundle.yml bytes inside a .zip must raise BundlerError.
The manifest bytes are decoded as UTF-8 explicitly, matching
``yamlio.load_yaml``'s "Could not read ..." contract, instead of
escaping as a raw ``UnicodeDecodeError``/``ReaderError`` traceback.
"""
artifact = tmp_path / "demo.zip"
with zipfile.ZipFile(artifact, "w") as archive:
archive.writestr("bundle.yml", b"\xff\xfe bundle \xc3\x28\n")
with pytest.raises(BundlerError, match="Could not read"):
_local_manifest_source(str(artifact))
def test_local_source_zip_utf16_manifest_rejected_like_directory(tmp_path: Path):
"""A well-formed UTF-16 manifest must fail the same way in a .zip.
``yamlio.load_yaml`` decodes strictly as UTF-8, so a UTF-16 bundle.yml
(the realistic PowerShell ``Out-File`` output) is rejected when read
from a directory. Feeding the zip bytes straight to PyYAML would let
its Reader honour the UTF-16 BOM and *accept* the same manifest,
making zip and directory sources diverge.
"""
artifact = tmp_path / "demo.zip"
manifest_text = "bundle:\n id: demo-bundle\n version: 1.0.0\n"
with zipfile.ZipFile(artifact, "w") as archive:
archive.writestr("bundle.yml", manifest_text.encode("utf-16"))
with pytest.raises(BundlerError, match="Could not read"):
_local_manifest_source(str(artifact))
def test_install_bundled_extension_from_zip_offline(tmp_path: Path):
"""End-to-end: build → install (offline, local .zip) → list → remove."""
project = make_project(tmp_path / "proj")
@@ -126,6 +126,50 @@ def test_active_integration_refuses_symlinked_specify_escape(tmp_path: Path):
assert active_integration(project) is None
def _write_marker(tmp_path: Path, payload: str) -> Path:
project = tmp_path / "proj"
(project / ".specify").mkdir(parents=True)
(project / ".specify" / "integration.json").write_text(
payload, encoding="utf-8"
)
return project
def test_active_integration_reads_default_integration(tmp_path: Path):
"""A marker carrying only ``default_integration`` must resolve.
``write_integration_json`` writes both ``integration`` and
``default_integration``, so a marker produced by the current CLI already
resolved through the alias. This covers the authoritative field on its own —
hand-edited, or written by anything that follows the shape of the canonical
reader (``integration_state`` line 199:
``state.get("default_integration") or state.get("integration")``).
"""
from specify_cli.bundler.lib.project import active_integration
project = _write_marker(tmp_path, '{"default_integration": "copilot"}')
assert active_integration(project) == "copilot"
def test_active_integration_prefers_default_over_legacy_alias(tmp_path: Path):
"""When both are present the authoritative field wins, matching
``integration_state``'s own ordering."""
from specify_cli.bundler.lib.project import active_integration
project = _write_marker(
tmp_path, '{"integration": "stale", "default_integration": "copilot"}'
)
assert active_integration(project) == "copilot"
def test_active_integration_still_reads_legacy_alias(tmp_path: Path):
"""Projects initialised by older versions carry only ``integration``."""
from specify_cli.bundler.lib.project import active_integration
project = _write_marker(tmp_path, '{"integration": "copilot"}')
assert active_integration(project) == "copilot"
def test_read_catalog_config_refuses_symlinked_specify_escape(tmp_path: Path):
from specify_cli.bundler.commands_impl import catalog_config as cc
+43
View File
@@ -565,6 +565,49 @@ def test_executable_env_var_devin_integration(monkeypatch):
assert args[0] == "/opt/devin"
def test_goose_integration_honours_extra_args(monkeypatch):
"""Goose gained ``build_exec_args()`` (the Goose item in #2416), so it must
honour the shared extra-args hook like every other dispatching integration."""
from specify_cli.integrations.goose import GooseIntegration
monkeypatch.setenv("SPECKIT_INTEGRATION_GOOSE_EXTRA_ARGS", "--debug")
args = GooseIntegration().build_exec_args("hi", output_json=False)
assert args == ["goose", "run", "--debug", "-t", "hi"]
def test_goose_extra_args_precede_canonical_flags(monkeypatch):
"""Extra args are applied before Spec Kit's canonical flags, matching the
opencode / codex / cursor-agent ordering.
Ordering parity only. This deliberately does not assert that a duplicated
canonical flag gets overridden: ``goose run`` is clap-derive based, and its
``--recipe`` / ``--model`` / ``--output-format`` are single-value args with
no ``args_override_self``, so duplicating one makes goose exit with "cannot
be used multiple times" regardless of which side wins the ordering.
"""
from specify_cli.integrations.goose import GooseIntegration
monkeypatch.setenv("SPECKIT_INTEGRATION_GOOSE_EXTRA_ARGS", "--debug")
args = GooseIntegration().build_exec_args("/speckit.specify", model="gpt-4o")
assert args[:3] == ["goose", "run", "--debug"]
assert args.index("--debug") < args.index("--model")
assert args.index("--debug") < args.index("--output-format")
assert args.index("--debug") < args.index("--recipe")
# Spec Kit itself must never emit a duplicate single-value flag.
for flag in ("--recipe", "--model", "--output-format"):
assert args.count(flag) == 1
def test_executable_env_var_goose_integration(monkeypatch):
"""GooseIntegration honours the executable env var."""
from specify_cli.integrations.goose import GooseIntegration
monkeypatch.setenv("SPECKIT_INTEGRATION_GOOSE_EXECUTABLE", "/opt/goose")
args = GooseIntegration().build_exec_args("hi")
assert args[0] == "/opt/goose"
assert args[1] == "run"
def test_executable_env_var_opencode_integration(monkeypatch):
"""OpencodeIntegration honours the executable env var."""
from specify_cli.integrations.opencode import OpencodeIntegration
@@ -83,3 +83,101 @@ class TestGooseCommandPlaceholderResolution:
assert "{SCRIPT}" not in prompt
assert "__AGENT__" not in prompt
assert "$ARGUMENTS" not in prompt
class TestGooseCliDispatch:
"""`goose` must produce argv for non-interactive dispatch.
`YamlIntegration` never overrode `build_exec_args()`, so Goose inherited the
`IntegrationBase` no-op returning `None`. Callers read `None` as "CLI
unavailable", so a workflow command/prompt step targeting Goose reported
"CLI not found or not installed" even with `goose` on PATH — the Goose item
in issue #2416. `goose run` supports `-t/--text`, `--recipe`,
`--params KEY=VALUE`, `--model` and `--output-format`.
"""
def test_build_exec_args_is_not_none(self):
integration = get_integration("goose")
assert integration.build_exec_args("/speckit.specify") is not None
def test_slash_command_maps_to_recipe(self):
integration = get_integration("goose")
args = integration.build_exec_args("/speckit.specify", output_json=False)
assert args[1] == "run"
assert "--recipe" in args
assert args[args.index("--recipe") + 1] == ".goose/recipes/speckit.specify.yaml"
# No trailing args -> no --params
assert "--params" not in args
def test_slash_command_arguments_map_to_params(self):
integration = get_integration("goose")
args = integration.build_exec_args("/speckit.specify add auth", output_json=False)
assert args[args.index("--params") + 1] == "args=add auth"
def test_dotted_extension_command_maps_to_recipe(self):
integration = get_integration("goose")
args = integration.build_exec_args("/speckit.git.commit msg", output_json=False)
assert args[args.index("--recipe") + 1] == (
".goose/recipes/speckit.git.commit.yaml"
)
def test_free_form_prompt_uses_text_flag(self):
"""goose has no `-p`; free-form text goes to `-t/--text`."""
integration = get_integration("goose")
args = integration.build_exec_args("just do it", output_json=False)
assert args[-2:] == ["-t", "just do it"]
assert "--recipe" not in args
def test_non_speckit_slash_prompt_is_not_treated_as_a_recipe(self):
"""`/help` is a goose session command, not a Spec Kit recipe.
`PromptStep` passes arbitrary `prompt:` strings to `build_exec_args`,
and the recipe branch synthesizes a *file path*, so slash text outside
the `speckit.` namespace must not become
`--recipe .goose/recipes/speckit.help.yaml` — `setup()` only ever
writes `command_filename(stem)` = `speckit.<name>.yaml`.
"""
integration = get_integration("goose")
args = integration.build_exec_args("/help", output_json=False)
assert "--recipe" not in args
assert "--params" not in args
assert args[-2:] == ["-t", "/help"]
def test_non_speckit_slash_prompt_is_not_promoted_to_a_recipe(self):
"""`/plan` is goose's own command and must not run speckit.plan.
`command_filename()` re-adds the `speckit.` prefix, so the old
unconditional call silently promoted the free-form goose command
`/plan` into a real Spec Kit recipe run. Dispatch always spells
commands `/speckit.plan` (`IntegrationBase.build_command_invocation`),
so no reachable recipe is lost.
"""
integration = get_integration("goose")
args = integration.build_exec_args("/plan the sprint", output_json=False)
assert "--recipe" not in args
assert args[-2:] == ["-t", "/plan the sprint"]
def test_bare_speckit_prefix_falls_through_to_text(self):
"""`/speckit.` alone has no stem and must not yield `speckit..yaml`."""
integration = get_integration("goose")
args = integration.build_exec_args("/speckit.", output_json=False)
assert "--recipe" not in args
assert args[-2:] == ["-t", "/speckit."]
def test_model_and_output_format_flags(self):
integration = get_integration("goose")
args = integration.build_exec_args("hi", model="gpt-4o", output_json=True)
assert args[args.index("--model") + 1] == "gpt-4o"
assert args[args.index("--output-format") + 1] == "json"
def test_recipe_target_matches_what_setup_writes(self, tmp_path):
"""Anti-drift: the dispatched `--recipe` path must be the file `setup()`
actually installed, so the two cannot diverge."""
integration = get_integration("goose")
manifest = IntegrationManifest("goose", tmp_path)
created = integration.setup(tmp_path, manifest, script_type="sh")
assert created
args = integration.build_exec_args("/speckit.specify hello")
recipe = args[args.index("--recipe") + 1]
assert (tmp_path / recipe).is_file(), f"{recipe} was not installed by setup()"
+299
View File
@@ -1,12 +1,29 @@
"""Tests for VibeIntegration."""
from unittest.mock import MagicMock
import yaml
from specify_cli.events import install_integration_events, remove_integration_events
from specify_cli.integrations import get_integration
from specify_cli.integrations.base import IntegrationBase
from specify_cli.integrations.manifest import IntegrationManifest
from .test_integration_base_skills import SkillsIntegrationTests
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover - Python < 3.11
import tomli as tomllib # type: ignore
def _vibe_manifest() -> MagicMock:
manifest = MagicMock(spec=IntegrationManifest)
manifest.files = {}
manifest.record_file = MagicMock()
manifest.record_existing = MagicMock()
return manifest
class TestVibeIntegration(SkillsIntegrationTests):
KEY = "vibe"
@@ -14,6 +31,274 @@ class TestVibeIntegration(SkillsIntegrationTests):
COMMANDS_SUBDIR = "skills"
REGISTRAR_DIR = ".vibe/skills"
def test_is_base_integration(self):
assert isinstance(get_integration("vibe"), IntegrationBase)
def test_multi_install_safe(self):
integration = get_integration("vibe")
assert integration.multi_install_safe is True
def test_canonical_to_native_events(self):
"""Vibe supports exactly three hook types: pre_tool, post_tool, post_agent."""
integration = get_integration("vibe")
assert integration.CANONICAL_TO_NATIVE == {
"pre_tool_use": "pre_tool",
"post_tool_use": "post_tool",
"stop": "post_agent",
}
def test_events_config(self):
integration = get_integration("vibe")
assert integration.events_config_file == ".vibe/hooks.toml"
assert integration.events_format == "toml-vibe"
def test_setup_creates_skill_files(self, tmp_path):
integration = get_integration("vibe")
manifest = IntegrationManifest("vibe", tmp_path)
created = integration.setup(tmp_path, manifest, script_type="sh")
skill_files = [path for path in created if path.name == "SKILL.md"]
assert skill_files
skills_dir = tmp_path / ".vibe" / "skills"
assert skills_dir.is_dir()
plan_skill = skills_dir / "speckit-plan" / "SKILL.md"
assert plan_skill.exists()
content = plan_skill.read_text(encoding="utf-8")
assert "{SCRIPT}" not in content
assert "{ARGS}" not in content
assert "__AGENT__" not in content
assert "__SPECKIT_COMMAND_" not in content, "unprocessed __SPECKIT_COMMAND_*__"
assert "/speckit." not in content, "skills agent must use /speckit-<name> not /speckit.<name>"
parts = content.split("---", 2)
parsed = yaml.safe_load(parts[1])
assert parsed["name"] == "speckit-plan"
assert parsed["user-invocable"] is True
assert parsed["disable-model-invocation"] is False
assert parsed["metadata"]["source"] == "templates/commands/plan.md"
def test_render_skill_unicode(self):
"""Test rendering a skill preserves non-ASCII characters."""
integration = get_integration("vibe")
rendered = integration._render_skill(
"constitution",
{"description": "Prüfe Konformität der Implementierung"},
"Body",
)
assert "Prüfe Konformität" in rendered
def test_setup_does_not_write_context_section(self, tmp_path):
"""The CLI no longer manages the agent context file — that is owned by
the opt-in agent-context extension. Setup must not create or touch it."""
integration = get_integration("vibe")
manifest = IntegrationManifest("vibe", tmp_path)
integration.setup(tmp_path, manifest, script_type="sh")
for path in tmp_path.rglob("*"):
if path.is_file():
text = path.read_text(encoding="utf-8", errors="ignore")
assert "<!-- SPECKIT START -->" not in text
def test_teardown_does_not_touch_existing_context_file(self, tmp_path):
"""A user-authored context file is left intact on teardown."""
integration = get_integration("vibe")
ctx_path = tmp_path / "AGENTS.md"
original = "# AGENTS.md\n\nUser content.\n"
ctx_path.write_text(original, encoding="utf-8")
manifest = IntegrationManifest("vibe", tmp_path)
integration.setup(tmp_path, manifest, script_type="sh")
integration.teardown(tmp_path, manifest)
assert ctx_path.read_text(encoding="utf-8") == original
def test_skills_do_not_have_argument_hint(self, tmp_path):
"""Vibe does not support argument-hint in skill frontmatter, so it must not be injected."""
integration = get_integration("vibe")
manifest = IntegrationManifest("vibe", tmp_path)
created = integration.setup(tmp_path, manifest, script_type="sh")
skill_files = [f for f in created if f.name == "SKILL.md"]
assert skill_files
for f in skill_files:
content = f.read_text(encoding="utf-8")
assert "argument-hint:" not in content, (
f"{f.parent.name}/SKILL.md unexpectedly has argument-hint frontmatter"
)
class TestVibeTomlMerging:
"""Behavioral tests for the toml-vibe hooks.toml generation and cleanup."""
def _install(self, tmp_path, events):
integration = get_integration("vibe")
manifest = _vibe_manifest()
install_integration_events(integration, tmp_path, manifest, events)
return integration, manifest
def _parse(self, tmp_path):
return tomllib.loads((tmp_path / ".vibe" / "hooks.toml").read_text(encoding="utf-8"))
def test_generated_toml_is_valid_and_schema_conformant(self, tmp_path):
self._install(tmp_path, {
"pre_tool_use": [{"command": "speckit.tdd.validate", "matcher": "Edit|Write"}],
"stop": [{"command": "speckit.session.finish"}],
})
data = self._parse(tmp_path)
hooks = data["hooks"]
assert len(hooks) == 2
by_type = {h["type"]: h for h in hooks}
assert set(by_type) == {"pre_tool", "post_agent"}
for h in hooks:
assert h["name"].startswith("speckit-")
assert isinstance(h["command"], str) and h["command"]
assert isinstance(h["timeout"], int)
# Canonical Claude-style regex matcher lands in Vibe's `match`
# field with the `re:` escape — never in a `matcher` field.
assert by_type["pre_tool"]["match"] == "re:Edit|Write"
assert "matcher" not in by_type["pre_tool"]
# HookConfig rejects `match` on post_agent hooks.
assert "match" not in by_type["post_agent"]
def test_wildcard_matcher_omitted(self, tmp_path):
self._install(tmp_path, {
"pre_tool_use": [{"command": "speckit.tdd.validate", "matcher": "*"}],
})
(hook,) = self._parse(tmp_path)["hooks"]
assert "match" not in hook
def test_unsupported_events_are_skipped(self, tmp_path, capsys):
self._install(tmp_path, {
"session_start": [{"command": "speckit.agent-context.update"}],
"pre_tool_use": [{"command": "speckit.tdd.validate"}],
})
hooks = self._parse(tmp_path)["hooks"]
assert [h["type"] for h in hooks] == ["pre_tool"]
assert "does not support 'session_start'" in capsys.readouterr().err
def test_multiple_handlers_get_unique_names(self, tmp_path):
"""Vibe drops duplicate hook names, so shared command stems must not collide."""
self._install(tmp_path, {
"pre_tool_use": [
{"command": "speckit.tdd.validate"},
{"command": "speckit.other.validate"},
],
})
hooks = self._parse(tmp_path)["hooks"]
assert len(hooks) == 2
names = [h["name"] for h in hooks]
assert len(set(names)) == 2
commands = " ".join(h["command"] for h in hooks)
assert "speckit.tdd.validate" in commands
assert "speckit.other.validate" in commands
def test_reinstall_is_idempotent(self, tmp_path):
events = {
"pre_tool_use": [{"command": "speckit.tdd.validate", "matcher": "Bash"}],
"stop": [{"command": "speckit.session.finish"}],
}
self._install(tmp_path, events)
first = self._parse(tmp_path)["hooks"]
self._install(tmp_path, events)
second = self._parse(tmp_path)["hooks"]
assert second == first
def test_merge_and_teardown_preserve_user_hooks(self, tmp_path):
config_path = tmp_path / ".vibe" / "hooks.toml"
config_path.parent.mkdir(parents=True)
user_block = (
'[[hooks]]\n'
'name = "deny-rm-rf"\n'
'type = "pre_tool"\n'
'match = "bash"\n'
'command = "guard-bash"\n'
)
config_path.write_text(user_block, encoding="utf-8")
integration, manifest = self._install(tmp_path, {
"pre_tool_use": [{"command": "speckit.tdd.validate"}],
})
merged = self._parse(tmp_path)["hooks"]
assert len(merged) == 2
assert any(h["name"] == "deny-rm-rf" for h in merged)
remove_integration_events(integration, tmp_path, manifest)
remaining = self._parse(tmp_path)["hooks"]
assert [h["name"] for h in remaining] == ["deny-rm-rf"]
def test_commands_carry_structured_output_envelope(self, tmp_path):
"""Vibe parses non-empty hook stdout as JSON (HookStructuredResponse);
plain text is reported as a hook failure. Every generated hook command
must therefore pass the hook_specific_output envelope to the dispatcher."""
self._install(tmp_path, {
"pre_tool_use": [{"command": "speckit.tdd.validate"}],
"stop": [{"command": "speckit.session.finish"}],
})
for hook in self._parse(tmp_path)["hooks"]:
assert hook["command"].endswith(" hook_specific_output"), hook["name"]
def test_windows_host_uses_cmd_quoting(self, tmp_path, monkeypatch):
"""Vibe runs hooks via create_subprocess_shell — cmd.exe on Windows,
where POSIX single quotes don't quote. A host interpreter path with
spaces must be double-quoted, never shlex-quoted."""
import specify_cli.events as events_mod
monkeypatch.setattr(events_mod, "_vibe_target_os", lambda: "cmd")
monkeypatch.setattr(
events_mod, "_resolve_interpreter",
lambda root: r"C:\Program Files\Python\python.exe",
)
self._install(tmp_path, {"pre_tool_use": [{"command": "speckit.tdd.validate"}]})
(hook,) = self._parse(tmp_path)["hooks"]
assert hook["command"].startswith('"C:\\Program Files\\Python\\python.exe" ')
assert "'" not in hook["command"]
def test_posix_host_keeps_shlex_quoting(self, tmp_path, monkeypatch):
import specify_cli.events as events_mod
# Pin the target: on a Windows CI runner _vibe_target_os() would
# return "cmd" and this test asserts the POSIX-host quoting path.
monkeypatch.setattr(events_mod, "_vibe_target_os", lambda: "host")
monkeypatch.setattr(
events_mod, "_resolve_interpreter",
lambda root: "/opt/my venv/bin/python3",
)
self._install(tmp_path, {"pre_tool_use": [{"command": "speckit.tdd.validate"}]})
(hook,) = self._parse(tmp_path)["hooks"]
assert hook["command"].startswith("'/opt/my venv/bin/python3' ")
def test_envelope_resolution(self):
from specify_cli.events import _context_envelope_for
integration = get_integration("vibe")
for event in ("pre_tool_use", "post_tool_use", "stop"):
assert _context_envelope_for(integration, event) == "hook_specific_output"
def test_emit_wraps_stdout_as_structured_response(self, capsys):
import json
from specify_cli.events import _emit_event_stdout
_emit_event_stdout("context line", "hook_specific_output")
data = json.loads(capsys.readouterr().out)
assert data == {
"decision": "allow",
"hook_specific_output": {"additional_context": "context line"},
}
# Empty stdout stays empty — Vibe treats it as "no response".
_emit_event_stdout("", "hook_specific_output")
assert capsys.readouterr().out == ""
def test_teardown_deletes_file_without_user_content(self, tmp_path):
integration, manifest = self._install(tmp_path, {
"pre_tool_use": [{"command": "speckit.tdd.validate"}],
})
assert (tmp_path / ".vibe" / "hooks.toml").is_file()
remove_integration_events(integration, tmp_path, manifest)
assert not (tmp_path / ".vibe" / "hooks.toml").exists()
class TestVibeUserInvocable:
def test_all_skills_have_user_invocable(self, tmp_path):
@@ -35,3 +320,17 @@ class TestVibeUserInvocable:
assert parsed.get("user-invocable") is True, (
f"{f.parent.name}/SKILL.md is missing user-invocable: true in frontmatter"
)
def test_all_skills_have_disable_model_invocation(self, tmp_path):
i = get_integration("vibe")
m = IntegrationManifest("vibe", tmp_path)
created = i.setup(tmp_path, m, script_type="sh")
skill_files = [f for f in created if f.name == "SKILL.md"]
assert skill_files
for f in skill_files:
content = f.read_text(encoding="utf-8")
parts = content.split("---", 2)
parsed = yaml.safe_load(parts[1])
assert parsed.get("disable-model-invocation") is False, (
f"{f.parent.name}/SKILL.md is missing disable-model-invocation: false in frontmatter"
)
+80
View File
@@ -302,6 +302,20 @@ class TestLoadAuthConfig:
with pytest.raises(ValueError, match="invalid host pattern"):
load_auth_config(cfg)
@pytest.mark.parametrize("host", ["gith?b.com", "[a-z].example.com"])
def test_unsupported_glob_metacharacters_raise(self, tmp_path, host):
cfg = tmp_path / "auth.json"
cfg.write_text(json.dumps({
"providers": [{
"hosts": [host],
"provider": "github",
"auth": "bearer",
"token_env": "X",
}]
}))
with pytest.raises(ValueError, match="invalid host pattern"):
load_auth_config(cfg)
def test_valid_star_dot_host_accepted(self, tmp_path):
cfg = tmp_path / "auth.json"
cfg.write_text(json.dumps({
@@ -344,6 +358,39 @@ class TestFindEntriesForUrl:
result = find_entries_for_url("https://myorg.visualstudio.com/project", [entry])
assert result == [entry]
@pytest.mark.parametrize(
"url",
[
"https://visualstudio.com/project",
"https://evilvisualstudio.com/project",
"https://visualstudio.com.evil.example/project",
],
)
def test_wildcard_does_not_match_apex_or_lookalikes(self, url):
entry = AuthConfigEntry(
hosts=("*.visualstudio.com",),
provider="azure-devops",
auth="basic-pat",
token_env="ADO_PAT",
)
assert find_entries_for_url(url, [entry]) == []
@pytest.mark.parametrize(
("pattern", "url"),
[
("gith?b.com", "https://github.com/org/repo"),
("[a-z].example.com", "https://a.example.com/file"),
],
)
def test_exact_hosts_do_not_apply_glob_semantics(self, pattern, url):
entry = AuthConfigEntry(
hosts=(pattern,),
provider="github",
auth="bearer",
token="sentinel",
)
assert find_entries_for_url(url, [entry]) == []
def test_no_match_returns_empty(self):
entry = _github_entry()
result = find_entries_for_url("https://evil.example.com/file", [entry])
@@ -1049,6 +1096,39 @@ class TestRedirectStripping:
assert new_req.headers.get("Authorization") is None
assert new_req.unredirected_hdrs.get("Authorization") is None
@pytest.mark.parametrize(
("hosts", "target", "expected_auth"),
[
(("*.example.com",), "https://api.example.com/asset", "Bearer tok"),
(("*.example.com",), "https://example.com/asset", None),
(("*.example.com",), "https://evil-example.com/asset", None),
(("gith?b.com",), "https://github.com/asset", None),
(("[a-z].example.com",), "https://a.example.com/asset", None),
],
)
def test_redirect_host_patterns_use_literal_safe_matching(
self, hosts, target, expected_auth
):
from specify_cli.authentication.http import _StripAuthOnRedirect
from urllib.request import Request
import io
handler = _StripAuthOnRedirect(hosts)
req = Request(
"https://source.example.org/file",
headers={"Authorization": "Bearer tok"},
)
new_req = handler.redirect_request(
req, io.BytesIO(b""), 302, "Found", {}, target
)
assert new_req is not None
auth = (
new_req.get_header("Authorization")
or new_req.unredirected_hdrs.get("Authorization")
)
assert auth == expected_auth
def test_https_to_http_same_host_redirect_rejected(self):
from specify_cli.authentication.http import _StripAuthOnRedirect
from urllib.request import Request
@@ -564,3 +564,38 @@ class TestGetInvokeSeparatorTolerance:
"integration_settings": {"droid": {"invoke_separator": "-"}},
})
assert common.get_invoke_separator(self._repo(tmp_path, body)) == "-"
@pytest.mark.skipif(
not (HAS_PWSH or _WINDOWS_POWERSHELL), reason="no PowerShell available"
)
def test_powershell_text_output_lists_available_docs(prereq_repo: Path) -> None:
"""Text mode must print a status line per document, like the twins.
`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 too — so `AVAILABLE_DOCS:` was emitted with nothing under it
while the bash and Python twins list every document.
"""
feat = prereq_repo / "specs" / "001-my-feature"
feat.mkdir(parents=True)
(feat / "plan.md").write_text("# plan\n", encoding="utf-8")
(feat / "research.md").write_text("# research\n", encoding="utf-8")
_write_feature_json(prereq_repo)
ps = _run(_ps_cmd(prereq_repo, "-IncludeTasks"), prereq_repo)
assert ps.returncode == 0, ps.stderr
assert "AVAILABLE_DOCS:" in ps.stdout
for doc in (
"research.md",
"data-model.md",
"contracts/",
"quickstart.md",
"tasks.md",
):
assert doc in ps.stdout, (doc, ps.stdout)
# The existing file reports [OK], the missing ones [FAIL].
assert "[OK] research.md" in _normalize_status_text(ps.stdout), ps.stdout
assert "[FAIL] quickstart.md" in _normalize_status_text(ps.stdout), ps.stdout
+21
View File
@@ -1,9 +1,12 @@
"""Regression guard: console symbols must remain importable from specify_cli."""
import logging
from specify_cli import (
console,
StepTracker,
select_with_arrows,
)
from specify_cli._console import logger as console_logger
def test_console_symbols_importable():
@@ -39,3 +42,21 @@ def test_select_with_arrows_raises_on_empty_options():
import pytest
with pytest.raises(ValueError, match="at least one option"):
select_with_arrows({})
def test_step_tracker_refresh_error_is_logged(caplog):
"""Regression: _maybe_refresh must log exceptions instead of silently swallowing."""
tracker = StepTracker("test")
def failing_refresh():
raise RuntimeError("simulated refresh failure")
tracker.attach_refresh(failing_refresh)
tracker.add("step1", "Step One")
with caplog.at_level(logging.DEBUG, logger=console_logger.name):
tracker.complete("step1", "done")
assert "Progress tracker refresh failed" in caplog.text
assert "RuntimeError: simulated refresh failure" in caplog.text
assert tracker.steps[0]["status"] == "done"
+88 -13
View File
@@ -12,6 +12,49 @@ WORKFLOWS_DIR = REPO_ROOT / ".github" / "workflows"
# inline shorthand (` - uses: x@sha`) used in catalog-assign.yml.
USES_RE = re.compile(r"^\s*(?:-\s*)?uses:\s*(?P<ref>\S+)", re.MULTILINE)
PINNED_SHA_RE = re.compile(r"@[0-9a-f]{40}$", re.IGNORECASE)
COMMUNITY_SUBMISSION_WORKFLOWS = (
(
"bundle",
"bundle-submission",
"bundles/catalog.community.json",
"docs/community/bundles.md",
"Modify only `bundles/catalog.community.json`",
),
(
"extension",
"extension-submission",
"extensions/catalog.community.json",
"docs/community/extensions.md",
"Do not modify any other files",
),
(
"preset",
"preset-submission",
"presets/catalog.community.json",
"docs/community/presets.md",
"Do not modify any other files",
),
)
def _create_pull_request_allowed_files(source_text: str) -> list[str]:
create_pr_match = re.search(
r"(?m)^ create-pull-request:\n(?P<body>(?:^ [^\n]*\n?)+)",
source_text,
)
assert create_pr_match is not None
allowed_files_match = re.search(
r"(?m)^ allowed-files:\n(?P<files>(?:^ - [^\n]+\n?)+)",
create_pr_match.group("body"),
)
assert allowed_files_match is not None
return [
line.strip().removeprefix("- ")
for line in allowed_files_match.group("files").splitlines()
if line.strip()
]
def test_github_actions_are_pinned_to_full_commit_shas():
@@ -41,22 +84,54 @@ def test_pinned_action_ref_accepts_uppercase_hex_sha():
)
def test_community_bundle_submission_automation_is_wired():
source = WORKFLOWS_DIR / "add-community-bundle.md"
compiled = WORKFLOWS_DIR / "add-community-bundle.lock.yml"
def test_community_submission_automation_is_wired_to_allowed_files():
assignment = WORKFLOWS_DIR / "catalog-assign.yml"
assert source.is_file()
assert compiled.is_file()
source_text = source.read_text(encoding="utf-8")
assignment_text = assignment.read_text(encoding="utf-8")
assert "names: [bundle-submission]" in source_text
assert "bundles/catalog.community.json" in source_text
assert "docs/community/bundles.md" in source_text
assert "verified: false" in source_text
assert "allowed-files:" in source_text
assert "bundle-submission" in assignment_text
for workflow, label, catalog_file, docs_file, instruction in (
COMMUNITY_SUBMISSION_WORKFLOWS
):
source = WORKFLOWS_DIR / f"add-community-{workflow}.md"
compiled = WORKFLOWS_DIR / f"add-community-{workflow}.lock.yml"
assert source.is_file()
assert compiled.is_file()
source_text = source.read_text(encoding="utf-8")
compiled_text = compiled.read_text(encoding="utf-8")
assert f"names: [{label}]" in source_text
assert catalog_file in source_text
assert docs_file in source_text
assert instruction in source_text
assert _create_pull_request_allowed_files(source_text) == [
catalog_file,
docs_file,
]
assert f'"allowed_files":["{catalog_file}","{docs_file}"]' in compiled_text
assert label in assignment_text
def test_community_submission_allowed_files_do_not_include_other_catalogs_or_docs():
allowed_by_workflow = {
workflow: set(
_create_pull_request_allowed_files(
(WORKFLOWS_DIR / f"add-community-{workflow}.md").read_text(
encoding="utf-8"
)
)
)
for workflow, *_ in COMMUNITY_SUBMISSION_WORKFLOWS
}
workflow_allowed_files = list(allowed_by_workflow.items())
for index, (workflow, allowed_files) in enumerate(workflow_allowed_files):
for other_workflow, other_allowed_files in workflow_allowed_files[index + 1 :]:
overlapping_files = allowed_files & other_allowed_files
assert overlapping_files == set(), (
f"{workflow} and {other_workflow} share allowed files: "
f"{sorted(overlapping_files)}"
)
def test_bug_test_workflow_provisions_python_dependencies():
+23
View File
@@ -4565,6 +4565,29 @@ steps:
errors = validate_workflow(definition)
assert any("invalid type" in e.lower() for e in errors)
@pytest.mark.parametrize("step_type", [["shell"], {"name": "shell"}])
def test_non_string_step_type_reports_error(self, step_type):
"""Unhashable YAML values must not crash registry membership checks."""
from specify_cli.workflows.engine import WorkflowDefinition, validate_workflow
definition = WorkflowDefinition(
{
"workflow": {
"id": "test",
"name": "Test",
"version": "1.0.0",
},
"steps": [{"id": "bad", "type": step_type}],
}
)
errors = validate_workflow(definition)
assert errors == [
f"Step 'bad': 'type' must be a string, got "
f"{type(step_type).__name__} ({step_type!r})."
]
def test_nested_step_validation(self):
from specify_cli.workflows.engine import WorkflowDefinition, validate_workflow