chore: sync fork with upstream v0.12.17 - #83
Merged
satwareAG-ironMike merged 135 commits intoJul 17, 2026
Conversation
* chore: bump version to 0.12.4 * chore: begin 0.12.5.dev0 development --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* docs: drop stale kimi KIMI.md->AGENTS.md migration note github#3097 made the agent-context extension a full opt-in and removed the KIMI.md -> AGENTS.md context migration from the kimi integration (_migrate_legacy_kimi_context_file and the context_file handling are gone). kimi's --migrate-legacy now only moves the skills directory. two lines in the integrations reference still promised the removed context migration; drop that clause so the docs match the code. * docs: clarify kimi legacy migration is skill naming, not directory names address review: the parenthetical said 'dotted->hyphenated directory names', but the migration is about skill naming (speckit.xxx -> speckit-xxx), matching the module docstring. reword to match.
…rides (github#3265) * fix(integrations): cursor-agent ignores executable/extra-args env overrides cursor-agent's build_exec_args() hardcoded self.key as argv[0] and never called _apply_extra_args_env_var(), so the documented SPECKIT_INTEGRATION_CURSOR_AGENT_EXECUTABLE (issue github#2596) and SPECKIT_INTEGRATION_CURSOR_AGENT_EXTRA_ARGS (issue github#2595) hooks were silently dropped — unlike every other CLI-dispatch integration (codex, devin). Route argv[0] through _resolve_executable() and apply the extra-args hook after the mandatory headless flags, mirroring the twins. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(integrations): pin extra-args insertion order for cursor-agent Per Copilot feedback: the extra-args override test only asserted the injected tokens were present, not that they land before Spec Kit's canonical --model / --output-format flags. Exercise build_exec_args with both a model and JSON output and assert the extra args are inserted before --model / --output-format (and the canonical flags stay intact and paired). Verified this fails if the _apply_extra_args_env_var call is moved after the flag extends. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5.3.0 to 5.4.0. - [Release notes](https://github.com/actions/setup-dotnet/releases) - [Commits](actions/setup-dotnet@9a946fd...26b0ec1) --- updated-dependencies: - dependency-name: actions/setup-dotnet dependency-version: 5.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* test: cover namespaced git branch templates
Assisted-by: Codex (model: GPT-5, autonomous)
* feat: support namespaced git branch templates
Assisted-by: Codex (model: GPT-5, autonomous)
* test: cover git branch template edge cases
Assisted-by: Codex (model: GPT-5, autonomous)
* fix: harden git branch template parsing
Assisted-by: Codex (model: GPT-5, autonomous)
* fix: address git branch template review feedback
Address Copilot review feedback for branch_prefix help text, namespaced GIT_BRANCH_NAME fallback behavior, final-segment validation docs, and Bash UTF-8 byte reporting.
Assisted-by: Codex (model: GPT-5, autonomous)
* fix: reject slug-scoped branch templates
Reject branch templates that place {slug} before {number}, because that makes namespace scanning depend on the generated feature slug and can reset numbering per feature name.
Assisted-by: Codex (model: GPT-5, autonomous)
* fix: ignore malformed timestamp refs when numbering
Align branch-number scanning with feature-branch validation so malformed timestamp-looking refs do not inflate sequential numbering. Also updates the stale git-common comment called out in review.
Assisted-by: Codex (model: GPT-5, autonomous)
… arg doesn't break multi-expression templates (github#3307) * fix(workflows): quote-aware interpolation so a literal }} in a filter arg doesn't break multi-expression templates github#3208/github#3228 hardened the single-expression fast path (_is_single_expression) so a literal {{ or }} inside a string argument like `| default('}}')` stays on the typed path. the multi-expression interpolation path was left on the old _EXPR_PATTERN regex, whose non-greedy `(.+?)}}` body stops at the first }} regardless of quoting. so a multi-expression template with a literal }} in any block captured a truncated body, hit the filter parser malformed, and raised ValueError. e.g. `{{ inputs.name }}: {{ inputs.missing | default('}}') }}` raised instead of interpolating. replace _EXPR_PATTERN.sub with _interpolate_expressions, which scans each block for a }} outside string literals - the same quote handling _is_single_expression already uses. plain-value passthrough (a literal }} in a resolved value, not an expression) is unchanged. add regression tests for a literal }} in the second block and in the first block, plus a literal {{ guard. * fix(workflows): surface malformed templates in interpolation instead of emitting verbatim address copilot review on github#3307: when the quote-aware scan finds no block-closing `}}` (e.g. an unbalanced quote in a filter arg swallowed the delimiter), fall back to the first raw `}}` in the tail and evaluate it, so the parser raises ValueError just as the old _EXPR_PATTERN.sub path did. only when there is no `}}` at all is the tail left verbatim (a genuinely unterminated `{{`, which the regex also could not match). keeps a typo failing loudly rather than being silently hidden. add a regression test for an unbalanced quote in a multi-expression template.
… of returning False (github#3323) _safe_compare coerced both operands to int/float unconditionally for <, >, <=, >=. any non-numeric string (an iso date, a version tag, a name) failed that coercion and the whole comparison silently returned False -- so `{{ inputs.d < '2026-02-01' }}` was False even when the date was earlier. only coerce when both operands look numeric; otherwise compare the original values, so two strings order lexicographically the way python does and two numeric strings still compare as numbers ("10" > "9"). a number vs a non-numeric string stays incomparable and yields False. add a regression test covering dates, plain strings, numeric strings, and the number-vs-string case.
…ore filtering (github#3331) CatalogStack.search() claimed a bundle id in `seen` only when the entry matched the query. so when the highest-precedence entry for an id did NOT match, a lower-precedence entry with the same id could match and be returned instead -- even though resolve()/install always use the highest-precedence entry. search advertised a bundle (name, version, source) the user could never actually get, contradicting the method's own docstring ("resolved at its highest-precedence source"). resolve every id to its highest-precedence entry first, then filter the resolved set by the query. search now agrees with resolve(): a query that only a shadowed lower-precedence copy matches returns nothing. add a regression test covering the shadowed-entry case.
…, not netloc) (github#3333) _validate_remote_url in bundler/services/adapters.py guarded on parsed.netloc, which is truthy for host-less URLs like "https://:8080" or "https://user@" even though they carry no host. so those passed the "must be a valid URL with a host" check. its docstring says it mirrors specify_cli.catalogs validation, but that site was already fixed to use hostname in github#3210/github#3227 and this twin was missed. switch to parsed.hostname (None for host-less URLs), matching catalogs.py. this guard runs before any network call, so it is a pre-flight safety check. add parametrized regression tests for the host-less forms plus a valid host+port sanity case.
) `validate` accepts a reject option case-insensitively (`o.lower() in {"reject", "abort"}`), so a gate authored as `options: [Approve, Reject]` passes validation. But `execute` compared the echoed choice case-sensitively, so picking `Reject` fell through to the approval path and silently ran downstream steps instead of aborting. Lower-case `choice` before the reject comparison so the runtime agrees with the validation that let the option through. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: bump version to 0.12.5 * chore: begin 0.12.6.dev0 development --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
) the goose recipe renderer emitted the prompt body under a bare '|' block scalar. yaml infers a plain block scalar's indentation from its first non-empty line, so a command body whose first line is itself indented (a markdown code block, a nested list item) made the parser expect that deeper indent for the whole block and reject the later, shallower lines - the generated .goose recipe then failed to parse. use an explicit '|2' indentation indicator so the block is always read at 2 spaces regardless of the body. added a regression test that round-trips an indented-first-line body through the yaml parser.
…Manager (github#3345) ConfigManager._load_yaml_config returned yaml.safe_load(...) or {}, which only guards falsy roots — a truthy non-mapping root (a YAML list or scalar) flows straight into _merge_configs, whose .items() raises AttributeError. get_config()/has_value()/get_value() then crash, and via should_execute_hook's blanket 'except Exception: return False' every config-based hook condition for that extension is silently disabled. Coerce a non-dict root to {}, mirroring the existing non-dict-root guard in get_project_config(). Hardens all three call sites in one place. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
github#3346) HermesIntegration.build_exec_args routed argv[0] through _resolve_executable() but never called _apply_extra_args_env_var(), so the documented per-integration extra-args env hook was silently dropped for hermes — the same class of bug fixed for cursor-agent in github#3265. Insert the hook after the base 'chat -Q' command and before Spec Kit's canonical -m/--json/-s/-q flags (mirrors opencode), so operator args can't displace or clobber the canonical flags. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* test: isolate integration test home Assisted-by: Codex (model: GPT-5, autonomous) * test: reduce registry manifest test repetition Assisted-by: Codex (model: GPT-5, autonomous) * test: clarify disjoint-manifest order rationale and guard safe set Add a >=2 precondition, explain why two install orders are tested (manifests are order-independent; the orders only vary the init path), and build the manifest map with a comprehension. * test: rotate init coverage for manifest isolation Assisted-by: Codex (model: GPT-5, autonomous) * test: assert integration home isolation Assisted-by: Codex (model: GPT-5, autonomous) * test: guard multi-install manifest rotations Assisted-by: Codex (model: GPT-5, autonomous)
* feat(scripts): add Python check-prerequisites PoC * fix(scripts): address check-prerequisites parity feedback * test(scripts): label PowerShell prerequisite parity cases --------- Co-authored-by: root <kinsonnee@gmail.com>
Add charter extension submitted by @Huljo to: - extensions/catalog.community.json (alphabetical order) - docs/community/extensions.md community extensions table Closes github#3322 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Zhiyao <zhiyao@ZhiyaodeMacBook-Air.local>
Update ralph extension submitted by @Rubiss: - extensions/catalog.community.json (version, download_url, speckit_version, tools, tags, updated_at) Closes github#3337 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>
…uire host) (github#3367) * fix(bundler): validate catalog URLs in `catalog add` (HTTPS-only, require host) add_source persisted remote catalog URLs without the HTTPS/host checks that specify_cli.catalogs (github#3210) and the bundler adapters (github#3333) enforce, and an unclosed IPv6 bracket escaped as a raw ValueError. Mirror the catalogs.py validation for http(s) schemes and wrap urlparse so malformed input raises BundlerError. Fixes github#3366 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: correct config filename and validation reference in comment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: bump version to 0.12.6 * chore: begin 0.12.7.dev0 development --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…ithub#3311) * feat(integrations): add post_process_command_content() hook for all format types Add post_process_command_content(self, content: str) -> str to IntegrationBase with a no-op default. Wire it into register_commands() for non-skills format types (Markdown, TOML, YAML) after format rendering, before writing to disk. Also applies to aliases rendered via the inject_name path (cline, forge). Skills-format agents are excluded to preserve the existing post_process_skill_content() path and avoid double-processing. This gives extension authors a clean per-agent content transformation seam for all 21 non-skills integrations that previously had no post-processing hook. Ref: github#3303 Assisted-By: 🤖 Claude Code * fix: initialize _integration before conditional branch Prevents potential UnboundLocalError if the non-skills guard is refactored without updating the alias path reference. Assisted-By: 🤖 Claude Code
Update ripple extension submitted by @chordpli: - extensions/catalog.community.json (version, download_url, description, requires.tools) - docs/community/extensions.md community extensions table Closes github#3354 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>
Update docguard extension submitted by @raccioly: - extensions/catalog.community.json (version, download_url, description, updated_at) - docs/community/extensions.md community extensions table Closes github#3355 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>
…log (github#3372) Add orchestration-task-context-management extension submitted by @benizzio to: - extensions/catalog.community.json (alphabetical order) - docs/community/extensions.md community extensions table Closes github#3356 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>
…hub#3347) AgyIntegration.build_exec_args returned [exe, '--print', prompt] without calling _apply_extra_args_env_var(), so the documented per-integration extra-args env hook was silently dropped for agy — same class as the cursor-agent fix github#3265. Append the hook after the positional prompt, matching the devin integration's shape. agy still ignores model/output as before. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…3348) ShellStep.validate() only checked that 'run' was present, so run: (null) or a GitHub-Actions-style list validated clean; execute() then str()-coerces the value and invokes it under shell=True, literally running 'None' or "['echo', 'hi']" as a command. Add a type check after the presence check, mirroring the command-step (github#3262) and gate options validation. Expression strings ('{{ ... }}') are strings, so they stay valid. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…3349) FanInStep.validate() only checked wait_for, so a non-mapping 'output' (a list or scalar) validated clean; execute() then silently coerces it to {}, so the author's declared aggregation keys vanish with COMPLETED status and no diagnostic. Reject a non-mapping output at validation, mirroring the command-step (github#3262) non-mapping fix. execute()'s defensive coercion is left in place for unvalidated callers. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…b#3352) workflow run/resume --json is contracted to emit a single JSON object on stdout, but every error path (workflow-not-found, invalid workflow, validation failure, execute/resume failure, and the shared _parse_input_values invalid-input error) used console.print, landing the human error text on stdout and corrupting the machine-readable stream. Route those messages through err_console when --json is set (a no-op for normal text mode), mirroring the stderr-only error routing already used by 'specify bundle' (_fail) and err_console elsewhere in this module. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ion (github#3353) On refresh (bundle update), install_bundle iterated only the new plan's components, so a component the previous version owned but the new one no longer ships was left installed on disk while being dropped from the rewritten record (contributed only holds plan.components). With no record referencing it, remove_bundle could never clean it up — permanently orphaned, violating the provenance invariant (FR-022). After the component loop, when refresh and a prior record exists, uninstall each previously-owned component absent from the new plan — unless another bundle still needs it (components_still_needed refcount, mirroring remove_bundle), in which case it stays installed and is simply de-attributed. Runs inside the existing try so a failed removal takes the same no-record-written rollback path. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
Syncs the satwareAG/spec-kit fork with upstream
github/spec-kittag v0.12.17, integrating 13 patch releases (v0.12.5 through v0.12.17, 138 commits).Closes #74, #75, #76, #77, #78, #79, #80, #81, #82.
Notable upstream changes integrated
ai_skillspreservation onuse(fix(integration): preserve ai_skills onusefor skills-mode Copilot github/spec-kit#3551)Conflict resolution
Four files conflicted during
git merge v0.12.17:pyproject.tomlversion = "0.12.17"tests/test_timestamp_branches.pyCHANGELOG.md## [satware-0.12.17]section, kept fork entries above upstream entriessrc/specify_cli/workflows/_commands.py_is_loopback_host()helper (commit 304e9e2) while adopting upstream's 6 new helper functions and restructuredworkflow_addinstall flow. Replaced upstream's inline loopback checks (6 sites: 4ip_address().is_loopback+ 2 hardcoded tuple checks) with the fork's helper.catalog.pyauto-merged cleanly; the fork'sStepRegistry.restore()method survived.Follow-up opportunities (out of scope for this sync PR)
catalog.pyhas 4 sites still using the hardcoded tuple("localhost", "127.0.0.1", "::1")- the exact pattern_is_loopback_hostwas created to replace. Predates this PR._reject_insecure_download_redirect(upstream code, line ~398) duplicates_is_loopback_hostlogic in a nested helper.Validation
pytest tests/ -x -q: 4415 passed, 114 skipped, 0 failuresruff check src/: All checks passedcheck-privacy-leaks.sh: No privacy violationscheck-upstream-sync.sh: OK: Up to date with upstream v0.12.17Post-merge updates
specs/metadata.json: version → 0.12.17, fork_version → satware-v0.12.17llms.txt: version pins updatedAssisted-by: opencode (model: glm-5.2, supervised)