mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
a3e183d069a2ef263296473582214023dc62788f
161 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e4318a3d1a |
fix(catalogs): validate the port in the shared catalog-URL validator, like its mirrors do (#3804)
* fix(catalogs): validate the port in the shared catalog-URL validator `CatalogStackBase._validate_catalog_url()` reads `parsed.hostname` inside its `try/except ValueError` but never reads `parsed.port`. `urlparse()` and `.hostname` do not perform port validation — only `.port` does — so a catalog URL with a non-numeric or out-of-range port passes validation. Every implementation that documents itself as mirroring this function already reads `.port` inside the same try: workflows/catalog.py (4 sites), bundler/services/adapters.py (2), bundler/commands_impl/catalog_config.py, and commands/bundle/__init__.py. The shared base — inherited by ExtensionCatalog and IntegrationCatalog — is the only one without it. The accepted URL then escapes as a raw `http.client.InvalidURL`, which is neither `urllib.error.URLError` nor `json.JSONDecodeError` (the only two the fetcher converts), so it surfaces as an unhandled traceback rather than the validator's normal error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(catalogs): describe both bad-port failure modes accurately The comment attributed both malformed-port cases to http.client.InvalidURL. Only a non-numeric port raises that (when the connection object is built); an out-of-range port constructs fine and fails later in the socket layer. Measured: example.invalid:notaport -> http.client.InvalidURL: nonnumeric port example.invalid:65536 -> HTTPSConnection() OK, connect() fails Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e916fd1b3b |
fix: preserve unreadable event config files (#3861)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
227b4f5e11 |
fix: normalize non-UTF-8 integration manifests (#3862)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
8394c8d536 |
[bug-fix] Fix upgrade-overwrites-copilot-skills: pass force=True to extension skill re-registration after upgrade (#3853)
* Fix upgrade-overwrites-copilot-skills: pass force=True to extension skill re-registration after upgrade Apply the remediation from the bug assessment on issue #3849. _register_extension_skills() had a skip guard that refused to overwrite existing SKILL.md files (protecting user customizations). In the upgrade path, setup() regenerates all core-template SKILL.md files first, then calls register_enabled_extensions_for_agent(). The guard then sees those freshly-written core files as 'existing' and skips every extension, leaving only core template content on disk. Fix: add force: bool = False to _register_extension_skills() and thread it through register_enabled_extensions_for_agent() and _register_extensions_for_agent(). In integration_upgrade(), pass force=True so extension content layers on top of the just-regenerated core files. The force flag is off-by-default so plain extension add still protects user-modified skill files. Refs #3849 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 'Unused local variable' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> * test: add end-to-end regression guard for upgrade-overwrites-copilot-skills (#3849) The existing regression tests in TestRegisterExtensionSkillsForceFlag exercise the new force parameter at the helper level, so without the fix they fail only with a TypeError (unknown kwarg) rather than on the user-facing behaviour. Add a command-level test that runs 'specify integration upgrade copilot --skills --force' end-to-end and asserts the installed git extension's SKILL.md is restored (with its extension content, not a bare core-template stub) when the skill directory already exists — the exact skill_dir_preexists path the bug depends on. The test fails on pre-fix source (the skill is never recreated) and passes with the fix, so it is a genuine behavioural regression guard rather than an API-surface check. Refs #3849 Assisted-by: GitHub Copilot (model: claude-opus-4.8, 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: Manfred Riem <15701806+mnriem@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> |
||
|
|
89126f3a33 |
fix(integrations): don't abort uninstall when the manifest can't be deleted (#3805)
`IntegrationManifest.uninstall()` guards every tracked-file `path.unlink()` with `except OSError: skipped.append(path)`, but the manifest's own `manifest.unlink()` is bare. The manifest is deleted *last*, so an undeletable manifest (read-only file, a directory left at the path, a Windows lock) raises after the tracked files are already gone. The caller loses the `(removed, skipped)` result and never runs its post-uninstall bookkeeping — reassigning the default integration, rewriting/removing `integration.json`, clearing init options — leaving a removed integration still recorded as installed. Report it in `skipped` like any other file we could not remove, mirroring the `path.unlink()` guard above and the same `except OSError: skipped.append(...)` pattern in kimi's legacy-directory cleanup. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b7b0e966cc |
fix(integrations): preserve non-UTF-8 VS Code settings (#3833)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
f8e474d6fd |
feat: first-class agent-native runtime hooks for integrations (#3704)
* feat: first-class agent-native runtime hooks for integrations * refactor: rework integration events per maintainer review - Rename hooks terminology to 'events' (events:, --events flag, events.py). - Use snake_case names for canonical events consistent with spec-kit vocabulary. - Fold event config adapters into integration classes via class attributes (CANONICAL_TO_NATIVE, events_config_file, events_format). - Lift event command-script resolution to core 'specify event run' command. - Split events sourcing from integration config writing. - Support first-class Copilot CLI events JSON generation under '.github/hooks/speckit.json'. - Rewrite and expand full test suite under 'tests/integrations/test_events.py'. Assisted-by: opencode (model: litellm/gemini-3.5-flash, autonomous) * fix(events): resolve ruff lint errors blocking CI Address Copilot review finding #18 (src/specify_cli/__init__.py event-command import missing # noqa: E402), #19 (unused console import in commands/event.py), and #20 (unused patch/yaml/Path/integration imports in test_events.py). Also fix two stray F541 f-string prefixes in _build_opencode_plugin that ruff flagged in the same job. Bump dev version 0.14.2.dev0 -> 0.14.2.dev1 and add a CHANGELOG entry per the AGENTS.md convention for Specify CLI __init__.py changes. Refs: PR #3704 Copilot inline review (findings #18, #19, #20) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): make generated native hooks actually execute Address Copilot review findings that left generated event hooks inert or schema-invalid after the rework: - #2: the resolved events map now carries an ordered list of handlers per event (dict[str, list[dict]]) so two extensions declaring the same event both run instead of the last one silently winning. collect_extension_events accumulates; every adapter emits one native entry per handler. - #6: Claude/Gemini/Qwen/Devin/Tabnine native schema accepts a single 'command' string, not command+args. Each adapter now renders one complete shell invocation of the dispatcher via _dispatcher_command(). - #7: Gemini measures hook timeouts in milliseconds; add events_timeout_unit attr and _native_timeout() so the 60s default becomes 60000ms instead of terminating the dispatcher after 60ms. - #4: _resolve_event_command_argv() replaces _extract_script_path() — scripts: values are command strings (e.g. 'scripts/bash/setup-plan.sh --json'), not bare paths. Resolves the project's sh/ps/py variant, splits safely into argv, and prepends the interpreter for .py. - #5: bundled-template fallback now uses _locate_core_pack()/_repo_root() (core_pack/commands, not the non-existent core_pack/templates/commands). - #16: all formatters use IntegrationBase.resolve_python_interpreter() so generated commands honor the project venv and never hard-code python3 (absent on Windows). The opencode TS plugin bakes in the same resolved interpreter. - #13: opencode TS plugin runEvent() now throws on failure instead of process.exit(2), which killed the OpenCode host process; only the failing hook is rejected. - #21: user YAML override is validated (event names, non-empty command strings) before returning; a malformed override is warned about and ignored rather than crashing installation on cfg.get(). Bump dev version 0.14.2.dev1 -> 0.14.2.dev2 (gemini/__init__.py change) and add a CHANGELOG entry. Refs: PR #3704 Copilot inline review (findings #2, #4, #5, #6, #7, #13, #16, #21) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): merge/teardown idempotency and data safety Address Copilot review findings on native-config merge and teardown: - #9: _has_marker now recurses into nested 'hooks' arrays so a matcher-group containing Specify-owned inner hooks is recognized and replaced on upgrade instead of accumulating duplicates. - #11: _merge_json_fragment strips ALL Specify-marked entries from every event before adding the new set, so an override that drops an event (pre_tool_use -> stop) removes the stale marked entry instead of leaving it active. - #3: an empty resolved map (--events false / disabled override) now runs the native-config removal path instead of early-returning, so prior Specify hooks are stripped. The shared dispatcher is left untouched (#10). - #14: teardown deletes a Spec-Kit-created config that is now empty of user content (rather than leaving '{}' that confused manifest.uninstall()), while preserving pre-existing configs with user hooks/settings. - #10: the shared .specify/events.py dispatcher is deleted only when no other installed event-capable integration's manifest still references it, so uninstalling one multi-install integration doesn't break the others. - #8: Copilot's .github/hooks/speckit.json now merges owned entries (with markers) into a pre-existing file instead of overwriting, and teardown removes only owned entries (deleting the file when no user hooks remain). - #22/#23: JSON/JSONC parse failures in native configs (Claude/Cursor/etc. and opencode.json) abort the merge with a warning instead of resetting user content to '{}'. - #12: write destinations are validated (symlinked-ancestor rejection + containment) before any bytes are written, so a symlinked .specify or native config directory can't redirect writes outside the repository. Refs: PR #3704 Copilot inline review (findings #3, #8, #9, #10, #11, #12, #14, #22, #23) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): honor enabled flag, refresh on extension lifecycle, strict command validation Address Copilot review findings on sourcing, validation, and lifecycle: - #1: collect_extension_events now honors the extension registry's 'enabled' flag — a disabled extension's events are skipped so disabling an extension actually deactivates its runtime hooks. Adds refresh_integration_events(), wired into extension add/remove/enable/disable, so installing, removing, enabling, or disabling an extension regenerates each installed event-capable integration's native event config (the documented install-after-init flow is no longer inert, and disabled/removed extension events are stripped). - #17: validate_events now requires 'command' to be a non-empty string, not merely truthy, so a value like 'command: [foo]' is rejected at manifest load instead of rendering into invalid native configuration. - #15: updated PR #3704 description to the implemented events terminology (.specify/events.py, events:, --events, integration-events.yml) replacing the stale bridge.py / runtime_hooks: / --hooks false / integration-hooks.yml references that no longer match the shipped API. (#21 — user YAML override validation — was addressed in the prior tier.) Refs: PR #3704 Copilot inline review (findings #1, #15, #17) Assisted-by: opencode (model: glm-5.2, autonomous) * revert: drop CHANGELOG.md/pyproject.toml version bumps from events fixes Per maintainer request, the events PR no longer carries CHANGELOG entries or pyproject version revs. This restores both files to their pre-PR ( |
||
|
|
6ef96373e2 |
fix(integrations): reject empty --commands-dir in generic raw_options (#3714)
* fix(integrations): reject empty --commands-dir in generic raw_options GenericIntegration._resolve_commands_dir has a parity gap: the parsed-options branch guards emptiness (`if commands_dir:`), but the raw_options fallback returned the value verbatim with no check. So `--integration-options= "--commands-dir="` (or `--commands-dir ""`) resolves to `""`, which makes setup() compute `dest = project_root / "" == project_root` and write every speckit command file (specify.md, plan.md, ...) directly into the PROJECT ROOT — silently bypassing the documented "--commands-dir is required" contract and polluting the repo root. Apply the same non-empty guard to the raw_options branch so an empty value falls through to the existing "required" ValueError on every input form. Non-empty values resolve exactly as before. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(integrations): reject a BLANK --commands-dir, not just an empty one Self-review follow-up: bare truthiness only closes the empty-string subset. A whitespace-only value passed both branches (verified: raw "--commands-dir ' '" returned ' ', parsed {"commands_dir": " "} returned ' '), so command files still landed in a directory literally named " " instead of failing with the documented "required" error. Require a non-BLANK value and normalize the padding, in the parsed branch as well as raw_options so the two cannot drift apart -- a padded but real value (" .myagent/cmds ") now resolves to ".myagent/cmds" rather than being rejected, matching how other padded config references are normalized. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(integrations): use strip() only to test blankness, return the value verbatim Address review feedback: normalizing with strip() changed EXISTING valid values, contrary to this PR's "no behaviour change for valid usage" claim -- a quoted `--commands-dir ' commands '` previously targeted the literal ` commands ` directory and would have started writing to `commands` instead. The blankness test still uses strip(), but the accepted value is now returned unchanged, so the fix stays limited to empty/blank input. Test updated accordingly: a padded non-blank value must round-trip verbatim (quoted in raw_options, since shlex.split() consumes unquoted padding before this code sees it). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3dad624e5d |
fix(integrations): render hyphenated /speckit-<name> for Droid (always-slash agent) (#3688)
* fix(integrations): render hyphenated /speckit-<name> for Droid (always-slash agent)
DroidIntegration is an always-skills agent: it installs commands as
.factory/skills/speckit-<name>/SKILL.md and its build_command_invocation
returns the hyphenated /speckit-<name>. But "droid" was missing from every
_invocation_style set, so is_slash_skills_agent("droid", True) returned False
and both HookExecutor._render_hook_invocation and `specify init` next-steps
fell through to the dotted /speckit.<name> form — a command Droid never
registers.
Add "droid" to ALWAYS_SLASH_AGENTS, matching its always-skills siblings
grok/trae/zed/devin (each added there by their own integration PR; droid's
#3587 omitted it).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(integrations): assert Droid is ALWAYS-slash (disabled case too)
Address review: the test only covered ai_skills=True, which would also pass
if Droid were miscategorized as CONDITIONAL_SLASH. Add the ai_skills=False
assertion — True there is what distinguishes an ALWAYS_SLASH agent from a
conditional one.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
89e204ca3c |
fix: use bounded read for integration catalog HTTP responses (#3763)
* fix(skills): match closing frontmatter delimiter on its own line
SkillsIntegration.setup parsed each command template's frontmatter with
raw.split("---", 2). A bare substring split stops at the first `---`
*anywhere*, so a template whose description embeds `---` (e.g.
"Separate sections with --- markers") truncated the parsed frontmatter:
later keys were dropped, the description fell back to the generic default,
and the leftover frontmatter spilled into the skill body.
Scan for the closing `---` on its own line instead, for both the
description parse and the body strip. The frontmatter block is parsed
unstripped so trailing newlines in literal (|) block scalars still survive,
and the body slice keeps the newline after the marker so output stays
byte-for-byte identical to the old split for well-formed templates.
Adds regression tests covering the dashed-description truncation and the
frontmatter-spilled-into-body cases.
* fix: use bounded read for integration catalog HTTP responses
The integration catalog fetch used unbounded resp.read() to read
HTTP responses into memory. A malicious or misconfigured catalog
server could return an arbitrarily large response causing OOM.
Replace with read_response_limited() capped at MAX_JSON_METADATA_BYTES
(1 MiB), consistent with how other JSON fetch paths in the codebase
(_version.py, _github_http.py, authentication/azure_devops.py) already
enforce bounded reads.
Pass error_type=IntegrationCatalogError so oversized catalogs are
caught by the existing per-entry recovery path in
_get_merged_integrations() rather than aborting the entire merge.
Add regression test verifying oversized responses are rejected as
IntegrationCatalogError and that healthy catalogs remain usable.
|
||
|
|
2e44ed60e8 |
fix(integrations): escape catalog metadata in discovery output (#3772)
* fix(integrations): escape catalog metadata in discovery output Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(integrations): escape unknown query IDs Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
655a3cb8ca |
fix(integrations): preserve native skill invocation prefixes (#3663)
* fix(integrations): use native dollar skill invocations Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02f9e138-da58-4a60-93b9-eae659d2aa19 * fix(integrations): preserve skill post-process idempotence Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02f9e138-da58-4a60-93b9-eae659d2aa19 * fix(integrations): preserve literal skill invocations Resolve generated command references with the active agent prefix instead of rewriting all slash-form text during post-processing. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02f9e138-da58-4a60-93b9-eae659d2aa19 * fix(integrations): preserve shared invocation prefix Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02f9e138-da58-4a60-93b9-eae659d2aa19 * fix(integrations): preserve install invocation prefix Pass dollar-style skill prefixes through bare-project integration installation and cover the shared template output. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02f9e138-da58-4a60-93b9-eae659d2aa19 * fix(integrations): preserve dollar refs everywhere Use agent-native invocation prefixes in extension command registration and dynamic shared-script command hints. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02f9e138-da58-4a60-93b9-eae659d2aa19 * fix(shared-infra): preserve dollar command hints Escape dollar-prefixed commands embedded in Bash strings and propagate the native prefix into installed Python command helpers. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02f9e138-da58-4a60-93b9-eae659d2aa19 * fix(shared-infra): render native helper prefixes Rewrite installed Bash and PowerShell formatter return expressions so direct callers receive the selected integration's native prefix. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02f9e138-da58-4a60-93b9-eae659d2aa19 * fix(skills): use invocation-neutral hook guidance Describe hook-derived references as command invocations so dollar-prefixed skills do not receive contradictory slash-command terminology. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02f9e138-da58-4a60-93b9-eae659d2aa19 * test(integrations): expect native fallback invocation Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02f9e138-da58-4a60-93b9-eae659d2aa19 * refactor(integrations): centralize invocation prefix selection Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02f9e138-da58-4a60-93b9-eae659d2aa19 * fix(integrations): add Kimi /skill: prefix and fix docstrings - Add SKILL_COLON_AGENTS frozenset and get_invocation_prefix() to _invocation_style.py so Kimi resolves to '/skill:' in skills mode - Switch invoke_prefix_for_integration() to use get_invocation_prefix() instead of the binary dollar/slash check - Update post_process_skill_content docstring (base.py) to cover both slash and dollar native invocation forms - Update _resolve_command_refs_in_skill docstring (presets/__init__.py) to document the dollar-prefixed result alongside slash forms Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) * fix(agents): use get_invocation_prefix for Kimi in register_commands Replace the binary is_dollar_skills_agent ternary with get_invocation_prefix so that __SPECKIT_COMMAND_*__ tokens in Kimi skill files resolve to /skill:speckit-<name> rather than /speckit-<name>. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) * fix(agents): remove unused is_dollar_skills_agent import Leftover from replacing the inline ternary with get_invocation_prefix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) * fix(integrations): use get_invocation_prefix in post_process_skill_content Replaces the binary is_dollar_skills_agent ternary with get_invocation_prefix so that Kimi's hook-command note is injected as /skill:speckit-git-commit from the start. This keeps _inject_hook_command_note idempotent for Kimi: the previous note with its native prefix now matches on repeated passes, preventing duplicate note injection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) * fix(presets): use get_invocation_prefix in _resolve_skill_command_refs Replace the binary is_dollar_skills_agent ternary with get_invocation_prefix so Kimi tokens resolve to /skill:speckit-* directly rather than /speckit-* (which previously relied on the broad post-process body replacement). Also fix test_restore_skill_preserves_dollar_command_refs to write raw_core with the unresolved __SPECKIT_COMMAND_PLAN__ token, exercising the resolver rather than bypassing it with a pre-resolved string. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) * docs(presets): document /skill: form in _resolve_skill_command_refs Add /skill:speckit-<cmd> to the docstring so the contract covers all three native prefix forms returned by get_invocation_prefix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) * test(integrations): add Kimi /skill: prefix coverage - test_skill_colon_prefix_core_command: resolve_command_refs with /skill: prefix - test_get_invocation_prefix_skill_colon: get_invocation_prefix returns /skill: for kimi (skills), / for kimi (non-skills), $ for codex, / for claude - test_kimi_skill_post_processing_is_idempotent: verifies Kimi's hook-command note is injected with /skill: prefix and does not duplicate on re-runs - test_installed_bash_formatter_uses_skill_colon_prefix: shared-infra bash formatter outputs /skill:speckit-plan when installed with /skill: prefix Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) * fix(kimi): use get_invocation_prefix in process_template, remove broad replacement process_template() was still using a binary is_dollar_skills_agent ternary to select between dollar and slash prefix, so Kimi tokens were emitted as /speckit-* and then corrected by a broad .replace('/speckit-', '/skill:speckit-') in KimiIntegration.post_process_skill_content(). That broad replacement would also rewrite any literal /speckit-* text in generated skill content, contrary to the PR's token-only behavior. - Use get_invocation_prefix(agent_name, invoke_separator == '-') in process_template() so Kimi tokens are emitted as /skill:speckit-* directly. - Remove the broad .replace() from KimiIntegration.post_process_skill_content(); it is now a no-op (tokens are already correctly prefixed at source). - Add test_process_template_kimi_uses_skill_colon_prefix to guard the fix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 02f9e138-da58-4a60-93b9-eae659d2aa19 Copilot-Session: 65ef91d9-4c31-4f31-a009-ed2093fe7f28 |
||
|
|
9602ad2edf |
fix(copilot): honor preset command template overrides (#3592)
* fix(copilot): honor preset command template overrides * fix(copilot): resolve canonical preset command names --------- Co-authored-by: Faqeha Noor <faqehanoor022@gmail.com> |
||
|
|
7fc5b236c8 |
feat: Add Alquimia AI integration (#2734)
* Add alquimia-ai as new integration: https://alquimia.ai * Fix test cases for alquimia-ai integration. Add alquimia-ai to workflow.yml * Add install url to alquimia-ai integration * Renamed alquimia-ai to alquimia (cli native denomination) * Fix unit tests for alquimia integration * Minor fix in alquimia integration * Fix typos and copilot findings * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+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> * Update tests cases and lint formatting * Final fixes * Rename alquimia_ai to alquimia module integration * Make cli optional for alquimiia integration * resolve review comments * Fix copilot review * Minor fixes: naming, remove unused code * Update tests cases. Fix issues * Fix unit tests * Add alquimia context to default agent-context extension. Update cli requirment to support workflows * Fix hints (suggestion) * Add Alquimia AI as agent in github issue template. Fix unit tests * Address review comments. Update docs * Update test cases --------- Co-authored-by: Eric Engstfeld <ericengstfeld@Erics-MacBook-Pro.local> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
683bfd00c9 |
fix: register extensions for the active integration only (#3459)
* fix: register extensions for the active integration only extension add registered commands for every detected agent, and integration upgrade back-filled enabled extensions for non-active integrations. Maintainer direction on #2948: treat the project as single-active. Only the active integration gets extension artifacts; use/switch rescaffold the target when the user selects it. - extension add now routes through the all-agents pass restricted to the active integration (only_agent), keeping detection and missing-skills-dir recovery safeguards. Projects without recorded init-options fall back to detection-based registration. - integration upgrade re-registers extensions only when upgrading the active integration, reversing the #2886 back-fill for non-active targets at maintainer request. Fixes #2948 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address review feedback on active-only extension registration - Restrict the extension-add active-integration fallback to projects with no recorded active key at all. A recorded but unsupported key (e.g. "generic", deliberately excluded from AGENT_CONFIGS) no longer falls back to registering every detected agent. - Apply the same single-active rule to preset command overrides: PresetManager._register_commands now scopes registration to the active integration via only_agent. - Add PresetManager.register_enabled_presets_for_agent, mirroring ExtensionManager.register_enabled_extensions_for_agent, and call it from integration use/switch/upgrade (active only) alongside the existing extension re-registration so presets are rescaffolded on activation instead of being written for inactive integrations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address second round of review feedback (priority order, fail-closed, docs) - register_enabled_presets_for_agent now processes presets in reverse priority order (lowest-precedence first) so the highest-precedence preset is written last and actually wins after `integration use` rescaffolds two overlapping preset command overrides. Verified this reproduces the previously reported reversed-priority bug and that the fix resolves it. - _register_commands_for_active_agent now checks for the "ai" key's presence separately from its value: a missing key still falls back to detection-based registration for all agents, but a recorded, malformed value (non-string or empty, e.g. [] or null) now fails closed (registers nothing) instead of being treated as "no active integration" or reaching AGENT_CONFIGS.get() with an unhashable key and raising TypeError. - Updated docs/reference/presets.md and docs/reference/integrations.md to describe active-only preset/extension registration and clarify that `integration use`/`switch` is the activation point for installed extensions and presets, and that `upgrade` only re-registers them for the active integration. Adds regression tests: two enabled presets overriding the same command with different priorities (priority winner must survive `use` rescaffolding), and a malformed recorded `ai` value ([]) for `extension add`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address third round of review feedback (multi-integration semantics) Fixes five deeper active-only registration bugs surfaced by Copilot review after |
||
|
|
99dc915ae3 |
fix: escape Rich markup in catalog list output (#3738)
The `catalog list` subcommands for workflows, workflow steps, presets, and integrations printed user-editable catalog fields (name/url/ description from the `*-catalogs.yml` files) through `console.print` with Rich markup enabled. Any bracketed content such as a description `Does [stuff] nicely` was parsed as a style tag and silently swallowed, and a malformed tag could raise while rendering. Route each untrusted field through the module's already-imported `escape` helper, matching the pattern already used by `extension catalog list`. Adds regression tests for all four commands that inject bracketed name/url/description and assert the brackets survive verbatim in the output. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ae82c74339 |
fix(kilocode): install commands under .kilo/commands (#3672)
* fix(kilocode): write commands to .kilo/commands * fix: guard Kilo legacy command migration --------- Co-authored-by: root <kinsonnee@gmail.com> |
||
|
|
391cc0dff8 |
fix(integrations): declare PiIntegration multi_install_safe (#3652)
* fix(integrations): declare PiIntegration multi_install_safe PiIntegration writes only to its isolated, static root .pi/prompts, disjoint from every other integration, yet never declared multi_install_safe — so it inherited the IntegrationBase default False, leaving `specify integration status` in a permanent unsafe-multi-install ERROR state when pi is co-installed alongside another agent. Add `multi_install_safe = True`, mirroring the isolated MarkdownIntegration cohort (qwen, shai, qodercli) and the kiro-cli #3471 fix. The parametrized registry isolation contracts auto-include pi and pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(integrations): list pi in the multi-install-safe reference table Declaring PiIntegration multi_install_safe means the reference table in docs/reference/integrations.md (which states it lists all currently declared multi-install-safe integrations) should include it. Add the alphabetized pi row with its .pi/prompts isolation path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e9dfe900f6 |
fix(integrations): declare OmpIntegration multi_install_safe (#3650)
* fix(integrations): declare OmpIntegration multi_install_safe OmpIntegration is a plain MarkdownIntegration whose files live only under its isolated, static root .omp/commands/, disjoint from every other integration. But it never declared multi_install_safe, so it inherited the IntegrationBase default False — leaving `specify integration status` in a permanent unsafe-multi-install ERROR state whenever omp is co-installed alongside another agent, with no acknowledgment path. Add `multi_install_safe = True`, mirroring the isolated MarkdownIntegration cohort (qwen, shai, qodercli, junie, kilocode) and the kiro-cli #3471 fix. The parametrized registry isolation contracts auto-include omp once the flag is set and pass (.omp/commands is isolated and its manifest disjoint). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(integrations): list omp in the multi-install-safe reference table Declaring OmpIntegration multi_install_safe means the reference table in docs/reference/integrations.md (which states it lists all currently declared multi-install-safe integrations) should include it. Add the alphabetized omp row with its .omp/commands isolation path. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
93dbf6d575 |
fix(integrations): recompute invoke_separator from retained parsed_options (#3664)
with_integration_setting recomputed invoke_separator from the raw
parsed_options argument. When only script_type changes (parsed_options and
raw_options both None), the previously-stored parsed_options are retained on
the setting, but the separator was derived from the None argument — dropping
an options-dependent separator (e.g. Copilot --skills -> "-") back to the
default ".", desynchronizing invoke_separator from the stored options.
Derive the separator from current.get("parsed_options") — the options
actually stored after the update — so it stays consistent in every branch.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
9fb467f8de |
fix(integrations): declare LingmaIntegration multi_install_safe (#3654)
LingmaIntegration writes only to its isolated, static root .lingma/skills, disjoint from every other integration, yet never declared multi_install_safe — inheriting the IntegrationBase default False and leaving `specify integration status` in a permanent unsafe-multi-install ERROR state when lingma is co-installed alongside another agent. Add `multi_install_safe = True`, mirroring the structurally-identical trae/zcode SkillsIntegrations and the kiro-cli #3471 fix. The parametrized registry isolation contracts auto-include lingma and pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5601830ba3 |
harden: bound HTTP reads and enforce strict redirects (#3140)
* harden: bound HTTP reads and enforce strict redirects Add a shared _download_security module (read_response_limited, is_https_or_localhost_http, size constants) and route the GitHub release and Azure DevOps token network reads through bounded reads so an oversized response can't exhaust memory. Add a strict_redirects mode to authentication.open_url: the redirect handler now rejects any redirect whose target isn't HTTPS (or HTTP to localhost), composing with the existing per-hop redirect_validator and auth-stripping. The Azure DevOps token POST is routed through that handler so a 307/308 cannot forward the client_secret body to a non-HTTPS host. Assisted-by: Codex (model: GPT-5, autonomous) * test: align HTTP fakes with bounded reads Assisted-by: Codex (model: GPT-5, autonomous) * fix: tolerate invalid token response encoding Assisted-by: Codex (model: GPT-5, autonomous) * test: align GHES fakes with bounded reads Assisted-by: Codex (model: GPT-5, autonomous) * test: reuse shared upgrade HTTP response helper Assisted-by: Codex (model: GPT-5, autonomous) * fix: include rejected redirect target in error Assisted-by: Codex (model: GPT-5, autonomous) * fix: enforce strict redirects by default Assisted-by: Codex (model: GPT-5, autonomous) * fix: close redirect credential and SSRF gaps Assisted-by: Codex (model: GPT-5, autonomous) |
||
|
|
a6743ab5e0 |
fix(integrations): validate cached catalog shape before returning it (#3627)
* fix(integrations): validate cached catalog shape before returning it
The catalog cache-read branch returned json.loads(cache_file) directly, skipping
the shape validation the fresh-fetch branch enforces (dict root + 'integrations'
mapping). A poisoned or older-format cache (e.g. {"integrations": []}) was
therefore returned as-is and later crashed with 'AttributeError: list object has
no attribute items' when the caller iterated integrations. Validate the cached
object the same way; the raised ValueError is already caught by the surrounding
handler, which drops the corrupt cache and refetches from source.
Test: a fresh-but-mis-shaped cache is dropped and the valid source refetched
(fails before: AttributeError).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(integrations): share one catalog-shape validator across cache and fetch
Address review: the cache-read path checked only that the payload was a
dict with a dict 'integrations', while the fresh-fetch path also required
'schema_version'. That asymmetry let an older/poisoned cache such as
{"integrations": {}} (no schema_version) bypass the format contract
instead of being dropped and refetched.
Introduce a shared `_catalog_shape_error()` helper and use it in both
paths so they enforce the same contract (dict + schema_version + dict
integrations). The fresh path still raises IntegrationCatalogError with
the "Invalid catalog format from <url>" prefix; the cache path still
raises ValueError (caught to drop+refetch). Add a test for the
missing-schema_version cache case.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(integrations): unit-test the shared catalog-shape validator directly
Replace the integration-level missing-schema_version cache test (which
was masked by multi-source merging — a sibling catalog source still
supplied the entry, so it passed regardless of the fix) with a direct
unit test of _catalog_shape_error. This deterministically proves both
paths now reject a payload missing schema_version, a non-dict
integrations, or a non-dict payload, and accept a well-formed one.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
470ac5b6e6 |
feat: add Factory Droid CLI integration (#822) (#3587)
Adds a skills-based integration for the Factory Droid CLI alongside the existing Claude/Codex skills agents. The integration scaffolds `.factory/skills/speckit-*` directories and documents the install step in the devcontainer post-create script via the official npm distribution (`npm install -g droid`), which matches the layout of every other CLI install block above and avoids executing an unverified remote shell installer. The integration is also added to the user-facing supported-agent table in `docs/reference/integrations.md` so the new key is discoverable from the published documentation, as required by the "Updating this documentation" guideline in AGENTS.md. Operator-supplied extra args via `SPECKIT_INTEGRATION_DROID_EXTRA_ARGS` are appended after the canonical Spec Kit flags so the canonical flags are always present in argv. The Factory Droid CLI parser uses last-wins duplicate-flag semantics (verified empirically against droid 0.175.0), so a later operator-supplied value may override the canonical one — this is a deliberate inversion of the cursor-agent / opencode / codex ordering. Includes: - `src/specify_cli/integrations/droid/__init__.py` (subpackage) - `tests/integrations/test_integration_droid.py` (46 tests, including regression coverage for the no-trailing-newline frontmatter fusion bug, idempotent skill injection, and env-var path resolution) - `integrations/catalog.json` entry + `updated_at` bump - Alphabetical registration in `src/specify_cli/integrations/__init__.py` and `tests/integrations/test_registry.py` - Devcontainer Droid install block via the npm distribution (`npm install -g droid`), replacing the earlier curl-based installer - User-facing supported-agent table row in `docs/reference/integrations.md` (key `droid`, `.factory/skills/` layout, `/speckit-<command>` invocation) - `AGENT_CONFIG` entry and matching alphabetical entries in `tests/test_agent_config_consistency.py` (`ISSUE_TEMPLATE_AGENT_KEYS`) and the three issue-template dropdowns (`agent_request.yml`, `bug_report.yml`, `feature_request.yml`) so `test_issue_template_agent_lists_match_runtime_integrations` keeps the runtime/template surfaces synchronized Closes #822 Assisted-by: Droid (oracle-reviewer) Assisted-by: Droid (model: MiniMax M3, autonomous) Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> |
||
|
|
5674fd03a9 |
fix(init): show hyphenated /speckit-<name> in Next Steps for Forge projects (#3642)
The post-init "Next Steps" panel renders recommended slash commands via
the nested `_display_cmd()`. It special-cased dollar-skills agents, kimi,
slash-skills agents, and cline, but not Forge. For a Forge project
`_display_cmd` fell through to `return f"/speckit.{name}"`, printing
`/speckit.constitution`, `/speckit.specify`, etc.
Forge only registers the hyphenated form (`/speckit-<name>`, per
`format_forge_command_name` / `ForgeIntegration.build_command_invocation`,
and the generated command-file tests already assert this), so the panel
told Forge users to run commands that don't exist under the dotted name.
Add `forge_skill_mode` alongside `cline_skill_mode` and include it in the
hyphenated-slash condition, mirroring how cline (also a non-skills
markdown agent with hyphenated commands) is handled. Other agents
unaffected.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d39f8fd5e5 |
fix(integrations): Cline dispatches hyphenated /speckit-<cmd> invocations (#3622)
Cline installs its slash-commands with hyphenated names (speckit-plan, speckit-git-commit) via format_cline_command_name + the hyphenated command_filename, but ClineIntegration inherited MarkdownIntegration's build_command_invocation, which builds the dotted /speckit.<cmd> — a name Cline never registered. Add a build_command_invocation override reusing format_cline_command_name, producing /speckit-<name>, mirroring the ForgeIntegration fix. Cline was the only remaining markdown integration with invoke_separator='-' + hyphenated command_filename that lacked the override. Tests assert Cline core + extension invocations are hyphenated, incl. args (fail before: dotted /speckit.plan / /speckit.git.commit). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d9e4565cf8 |
fix(integrations): escape Rich markup in --integration-options error messages (#3458)
* fix(integrations): exit cleanly on malformed --integration-options quoting
_parse_integration_options called shlex.split(raw_options) unguarded. an
unbalanced quote (e.g. --integration-options='--commands-dir "foo') makes
shlex raise ValueError('No closing quotation'), so a raw traceback escaped
instead of the typer.Exit(1) error every other bad-input path in this function
produces. reachable from specify init and every integration install/switch/
upgrade/migrate that accepts --integration-options.
wrap the split and convert ValueError into the same clean CLI error. added a
regression test; confirmed it fails on the pre-fix code (raw ValueError).
* escape user-controlled values in integration-options error messages
the malformed-quoting handler (and the unexpected/unknown option
branches) interpolate raw_options/token into console.print. a value
carrying an unbalanced rich tag like '--commands-dir "[/red]foo' first
trips the intended shlex ValueError, but the error print then raises
rich.errors.MarkupError and leaks a traceback anyway. escape all three
before printing so the clean typer.Exit survives.
added a regression covering both the shlex path and a bare markup token.
* address review: drop redundant, mis-described markup test
The removed test's docstring claimed the shlex failure branch
interpolates raw_options into console.print; it only prints {exc},
which never contains the caller's markup. Its two assertions also
duplicated test_bad_option_token_with_rich_markup_exits_cleanly (the
'[/red]foo' case) and the shlex-path case already covered by
test_unbalanced_quote_exits_cleanly. The real change here remains the
escape() of the two user-controlled token prints.
|
||
|
|
2a0ada9a6a |
feat(scripts): port create-new-feature, setup-plan and setup-tasks to Python (#3386)
* feat(scripts): port create-new-feature, setup-plan and setup-tasks to Python Ports the three core workflow scripts to Python as part of #3280, following the check-prerequisites PoC pattern from #3302. Adds resolve_template() to the shared common.py module and parity tests that run bash and Python side by side. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(tests): treat only None env as unset in parity run helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): fall back to directory scan on any registry error, skip hidden preset dirs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(templates): add py: lines for setup_plan and setup_tasks Ships with the scripts they reference; the remaining templates got their py: lines in #3403. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: support py variant in skills placeholder resolver resolve_skill_placeholders only accepted sh/ps, so a py init option fell into the fallback path and {SCRIPT} rendered without an interpreter prefix. Accept py and prefix the resolved interpreter, matching process_template. Also guard ps_cmd against a missing PowerShell with a clear assert. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: pin clean-error behavior for invalid --number Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(scripts): reword unused-arg comment to match implementation The loop accepts and silently ignores extra positional args (it doesn't build a collected list); match the wording to what the code and setup-plan.sh actually do. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: fall back when configured script variant is missing from frontmatter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): reject signed/whitespace --number values to match bash 10# parity The bash twin uses $((10#$BRANCH_NUMBER)), which rejects signed and whitespace-padded values. Python's int() accepted them (e.g. -1), producing a malformed -01-... prefix that sequential scans ignore. Restrict --number to unsigned decimal digits before conversion, and pin the parity with a bash-comparison test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): complete Python port installation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(integrations): fall back for missing script variants Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: make Python script checks platform-aware Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix Windows Python command invocation parity Use PowerShell's call operator for spaced Python interpreter paths and align setup-tasks missing-template errors across script variants. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): preserve cross-platform Python parity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: reject signed PowerShell feature numbers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): align feature number range Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): reject exhausted feature numbers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): complete create feature parity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): align create feature outputs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): harden cross-platform parity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): keep truncation JSON clean Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): align setup failure parity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): close parity edge cases Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): propagate PowerShell setup errors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): harden fallback resolution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): stabilize PowerShell fallbacks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): complete setup-plan parity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(cli): require runnable script fallbacks Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(cli): preserve shell fallback without preference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): restore help and symlink parity - setup-tasks.ps1: check -Help before unknown-argument validation so '-Help --bogus' exits 0 like the Bash/Python variants - common.py: strip the repo root prefix lexically in persist_feature_json instead of resolve(), so a symlinked specs/ still persists the relative 'specs/NNN-name' path the Bash/PowerShell helpers store Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(scripts): align persist-hint quoting with shlex.quote - create-new-feature.sh: replace printf %q with a shell_quote helper that emits shlex.quote-identical output, so the persistence hints stay byte-identical between the Bash and Python variants (printf %q output also varies between bash versions) - promote the negative --number test to an all-variants parity test now that Bash and PowerShell reject signed values consistently - add a spaced-repo-path parity test for the persistence hints Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
74662cffad |
feat: update Bob integration to skills-based layout for Bob 2.0 (#3415)
* feat: update Bob integration to skills-based layout for Bob 2.0 Bob 2.0 replaces the command-based workflow (.bob/commands/*.md) with a skills-based layout (.bob/skills/speckit-<name>/SKILL.md), matching the pattern used by Claude Code, Codex, and other skills-first agents. - Switch BobIntegration from MarkdownIntegration to SkillsIntegration - Update folder/dir from .bob/commands to .bob/skills - Change extension from .md to /SKILL.md (skills layout) - Add --skills option (default: True) consistent with Codex pattern - Update tests to inherit from SkillsIntegrationTests (28 tests pass) - Bump catalog entry to version 2.0.0 with updated description Assisted-by: IBM Bob (model: claude-sonnet-4-5, autonomous) * PR comments fix: keep old Bob 1 commands till next release * Copilot suggested change Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * feat(bob): address copilot comments, make skills layout default, demote legacy commands to opt-in * fix(bob): honor legacy_commands in ai_skills persistence and add bob to ALWAYS_SLASH_AGENTS - init.py: suppress ai_skills=True when --legacy-commands is passed so extensions and presets target .bob/commands, not .bob/skills - _invocation_style.py: add 'bob' to ALWAYS_SLASH_AGENTS so init next-steps and hook invocations always show /speckit-<name> (skills is the default layout; no ai_skills flag required) * Copilot suggestion Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix(bob): extend IntegrationBase directly to avoid false isinstance(SkillsIntegration) - bob/__init__.py: switch BobIntegration base from SkillsIntegration to IntegrationBase; add _BobSkillsHelper for skills-mode delegation; set invoke_separator='-' explicitly; set _skills_mode flag in setup() so consumers can derive the effective mode without isinstance checks - _helpers.py: replace isinstance(integration, SkillsIntegration) guard with getattr(_skills_mode) so legacy-commands mode does not persist ai_skills=True - _invocation_style.py: remove 'bob' from ALWAYS_SLASH_AGENTS — Bob 2.0 skills are invoked via natural language, not /skill-name slash commands - integrations/catalog.json: advance updated_at to 2026-07-15 * fix(lint): remove unused SkillsIntegration import from _helpers.py * Copilot suggested change Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * feat(bob): add bob skills integration with registrar-based mode detection * address 3 comments from copilot * feat(bob): update registrar config to use legacy commands layout * fix lint * Suggested fix from Copilot Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix pr comment * fix pr comment * fix pr comment * refactor(bob): resolve skills mode via base-class hooks + fix command-ref separators Rework the dual-mode handling introduced for Bob 2.0 so an integration's internal representation never leaks into shared init/install/upgrade code, and fix the legacy command-reference separator surfaced in review. Base-class contract: - Add IntegrationBase.is_skills_mode(parsed_options) — the single hook the shared machinery consults to decide whether to persist ai_skills and render skill invocations. SkillsIntegration returns True; Copilot honors --skills / self._skills_mode; Bob returns `not legacy_commands`. - Add IntegrationBase.invoke_separator_for_mode(skills_enabled) — resolves the command-ref separator from a project's persisted mode for registration paths that only have the ai_skills flag (no CLI parsed_options). Default is behavior-preserving; Bob maps skills->"-", legacy->".". - BobIntegration stays on IntegrationBase (mirroring Copilot, the other dual-mode agent) and delegates setup() to internal _BobSkillsHelper / _BobMarkdownHelper. Removes the _skills_mode method and all isinstance(SkillsIntegration) / callable(_skills_mode) probing from _helpers.py and init.py. Fix legacy separator (review feedback): CommandRegistrar.register_commands and PresetManager._resolve_skill_command_refs previously read the single static AGENT_CONFIGS[key]["invoke_separator"], so legacy .bob/commands/ extension and preset command refs rendered /speckit-<cmd> instead of Bob 1.x /speckit.<cmd>. Both now resolve the separator per project mode via invoke_separator_for_mode. Tests: add regression coverage for the is_skills_mode / invoke_separator_for_mode hooks and legacy extension command-ref separators; normalize a width-sensitive workflow assertion to match its siblings. Full suite green. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf * fix(bob,copilot): address review — preserve legacy layout, dual-mode separators, extension-skill token resolution Addresses PR review 4716036212 (3 comments): 1. Bob legacy-install regression: `use`/`switch`/`upgrade` on an existing Bob 1.x project (only `.bob/commands/` on disk, no stored `legacy_commands`) called `is_skills_mode(None)` -> True and rewrote `ai_skills=True`, silently switching extension/command-reference handling to the skills layout. `is_skills_mode` now takes an optional `project_root`; Bob preserves an already-installed legacy layout until an explicit upgrade creates `.bob/skills/`. A fresh project still defaults to skills. 2. Copilot dual-mode separator: `invoke_separator_for_mode` was inherited from the base (mode-independent) and returned Copilot's static `.`, so preset/extension command refs in a Copilot skills project rendered `/speckit.<name>` instead of `/speckit-<name>`. Override it on Copilot to track the persisted `ai_skills` state, consistent with `build_command_invocation` and `effective_invoke_separator`. 3. Bob extension-skill command-ref tokens: verified that merging main's generic `_resolve_command_ref_tokens` (#3544) resolves Bob's tokens via the `CONDITIONAL_SLASH_AGENTS` path (`/speckit-<name>`); added Bob to the command-ref regression parametrize plus dedicated Bob use-path tests. All tests pass (full suite green; merged with current main incl. #3544). Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf * fix(bob): resolve command-ref separator with project-aware mode before shared-infra refresh (review #3415) The `use`/`switch` paths refresh shared infrastructure via `_with_integration_setting()` / `_invoke_separator_for_integration()`, which previously resolved the invoke separator through `effective_invoke_separator` / `is_skills_mode` WITHOUT a project_root. For a pre-PR Bob 1.x project (.bob/commands/ on disk, no stored options), this defaulted to the skills "-" separator and rewrote rendered shared-template command refs to /speckit-*, even though ai_skills stayed false. Thread project_root through effective_invoke_separator, the two runtime helpers, and every call site so Bob's on-disk legacy detection governs the separator before shared infra is refreshed. Add a rendered-shared-template regression test covering `use --force`. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf * fix(bob): scope persisted ai_skills flag to active agent when resolving command-ref separator (review #3415) `register_commands` runs once per detected agent, but the persisted `ai_skills` flag describes only the active integration (`opts["ai"]`). When another agent (e.g. Copilot) is active in skills mode while a legacy `.bob/commands` layout is also present, the previous code passed that global `True` to Bob's `invoke_separator_for_mode`, rewriting Bob 1.x command refs to `/speckit-*` instead of `/speckit.*`. Only consult the persisted flag for the agent it describes (`opts["ai"] == agent_name`); otherwise resolve the separator from the agent's own project-aware `effective_invoke_separator(None, project_root)`. Add regression tests covering the mismatched-active-agent case and a control for Bob-active skills mode. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf * fix(bob): detect Spec Kit layout from managed artifacts, not any skills dir (review #3415) Two related mis-detections from review 4723246468: 1. `BobIntegration.is_skills_mode` treated the mere presence of a `.bob/skills/` directory as proof the project is skills-based. A legacy Spec Kit install (managed `.bob/commands/speckit.*.md`) that also carried unrelated Bob 2 skills would be misclassified as skills, so `integration use bob` persisted `ai_skills` and rewrote shared refs. Now the layout is inferred from managed Spec Kit artifacts: legacy/command mode only when managed `speckit.*.md` command files exist and no managed `speckit-*` skill dirs do. 2. The `register_commands` separator for an inactive agent used a disk-based `effective_invoke_separator(None, project_root)` fallback that could pick the skills separator even though the registrar writes the static command layout (`.bob/commands/*.md`). Inactive agents now resolve the separator from the registrar's actual output layout (`extension == "/SKILL.md"`), so command-layout files keep `/speckit.*` refs regardless of sibling dirs. Update the affected hook/E2E tests to use managed artifacts and add regression tests for the mixed-layout and inactive-registrar scenarios. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf * fix(bob): apply managed-artifact detection on upgrade + consistent skill post-processing (review #3415) Two issues from review 4723782860: 1. `BobIntegration.setup()` resolved the layout via `is_skills_mode(parsed_options)` WITHOUT `project_root`, so `integration upgrade bob` on a Bob 1.x install (managed `.bob/commands/speckit.*.md`, no stored options) ignored the existing command files, generated skills, and stale-deleted the legacy commands — silently migrating the project. Pass `project_root` so the same managed-artifact detection used by `use` also governs upgrades. 2. Only `_BobSkillsHelper` overrode `post_process_skill_content` to suppress the shared slash-command hook note. Preset/extension skill generators call that hook on the registered `BobIntegration`, which inherited `IntegrationBase`'s note-injecting default. Repeat the no-op (delegating to the skills helper) on the registered class so every Bob skill-generation path is consistent with intent-activated core Bob skills. Add regression tests for the upgrade-preservation and post-processing paths. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf * feat(bob): add --skills migration opt-in; fix separator + manifest loss (review #3415) Address review #3415 (4724160183): - Comment 1: Add an explicit `--skills` opt-in to BobIntegration. It forces the skills layout over on-disk auto-detection, giving legacy Bob 1.x installs a supported migration path (`integration upgrade bob --integration-options="--skills"`). `--skills` and `--legacy-commands` are mutually exclusive (clean exit-1 error). - Comment 2: In CommandRegistrar.register_commands, derive the command-ref separator from the output layout (agent_config["extension"]) for the active agent too, not the persisted ai_skills flag. A command-layout file (.bob/commands/*.md, .github/agents/*.agent.md) always renders /speckit.*; only a /SKILL.md scaffold uses /speckit-*. Dual-layout agents (Bob, Copilot) write skills via their own setup()/skills path, so register_commands only ever emits their command-layout files. - Comment 3: Update docs/reference/integrations.md Bob entry to document the skills-based default (.bob/skills/), the deprecated --legacy-commands opt-out, and the --skills migration path. Also fix a latent manifest-loss bug surfaced by the migration path: the upgrade Phase 2 stale-file cleanup built a throwaway manifest sharing the integration key and called uninstall(), which always deleted {key}.manifest.json. Any layout-shrinking upgrade (e.g. legacy->skills) thus wiped the freshly-saved manifest, leaving the project untracked and un-upgradeable. uninstall() now takes remove_manifest (default True); the stale-cleanup pass passes False. Adds regression tests for the --skills opt-in, mutual exclusion, corrected active-agent separator, remove_manifest=False, and an end-to-end legacy->skills migration that verifies the manifest survives and the project remains upgradeable. Full suite: 4555 passed, 5 skipped. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf * docs(agents): align token-resolution comment with output-layout separator rule (review #3415) Address review #3415 (4725516805). The comment above resolve_command_refs still described the removed state-based behavior ("resolve it from the integration using the project's persisted skills state"). Update it to describe the output-layout rule that register_commands now uses: _sep is derived from the layout this registrar writes (a /SKILL.md scaffold uses the skills separator; a command-layout file uses the command separator), not the persisted ai_skills state. Comment-only change; no behavior change. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf * fix(bob): reconcile extension artifacts on layout change (review #3415) When a dual-mode agent (Bob) flips between the legacy commands layout and the skills layout during `integration upgrade` (via `--skills` / `--legacy-commands`), the old layout's extension command/skill files were left orphaned: Phase 2 stale cleanup only removes files tracked by the *integration* manifest, while extension artifacts are tracked in the extension registry. Detect the layout flip by comparing whether the old vs new manifest tracks a `/SKILL.md` scaffold, and when it changed, unregister the agent's extension artifacts before the existing re-registration so they are recreated in the new layout (and the per-agent registry is updated). Preset artifacts are documented as a known, pre-existing cross-cutting gap: no agent-scoped preset re-registration exists in use/switch/upgrade for any agent, so reconciling them is out of scope for this Bob migration. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf * fix(bob): reject layout migration when preset overrides are installed (review #3415) A command↔skills layout change during `integration upgrade` cannot reconcile preset artifacts: presets track their command/skill files in per-preset `registered_commands`/`registered_skills` metadata, and there is no agent-scoped preset re-registration anywhere in the CLI. Migrating would delete a preset's old-layout files without recreating them in the new layout and leave the preset registry claiming artifacts that no longer exist. Detect the intended layout via `is_skills_mode` (so a plain same-layout upgrade is unaffected) and, when it flips while preset overrides are installed for the agent, reject the upgrade *before any mutation* with an actionable error pointing at the remove → upgrade → reinstall workaround. Extension artifacts are still reconciled for the safe (no-preset) case. Adds a regression test and documents the migration caveat in the Bob integration reference entry. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf * fix(bob): restrict layout reconciliation to the active integration (review #3415) `integration_upgrade` supports upgrading a secondary (non-active) integration, but the layout-change extension reconciliation was unsafe there. `ExtensionManager.unregister_agent_artifacts()` treats the unscoped per-extension `registered_skills` list as belonging to the passed agent and, when that agent's skills directory is absent, falls back to scanning every agent's skills directory — so reconciling a secondary Bob layout flip could delete or untrack the *active* agent's extension skills. The subsequent re-registration cannot repair that because extension skill rendering is intentionally scoped to the active agent (#2948). Gate the unregister-before-register reconciliation on `installed_key == key` so it only runs for the active integration. Secondary agents only ever have extension command files (skills are active-agent-only), which the existing re-registration rewrites in place, so skipping the unregister orphans nothing new. Adds a regression test asserting a secondary Bob layout change leaves the active agent's extension skill intact on disk and in the registry. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf * fix(bob): fail closed when preset registry is unreadable (review #3415) Address review 4744636079: - _migrate_commands: the preset guard previously failed *open* — a registry read/parse error returned an empty "no presets" list, so a --force layout-changing upgrade could delete preset-overridden command files while their registry state was unknown. Read the registry file directly and raise _PresetRegistryUnreadableError on any read/parse failure or malformed structure, rejecting the migration before any mutation. A genuinely absent registry still returns [] (safe). - bob: correct the is_skills_mode docstring — upgrade *does* run setup(); disk detection is needed because legacy Bob 1.x installs never persisted a legacy_commands option, so the stored mode is unavailable. - tests: add fail-closed E2E (corrupted registry rejected, valid-empty allowed) plus a unit test for _installed_presets_affecting_agent covering absent / corrupted / malformed / valid / affecting-agent cases. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf * fix(bob): fail closed on malformed preset entries too (review #3415) Address review 4745191015: the preset guard read a parseable registry but silently skipped malformed per-preset metadata and treated a malformed registered_commands value as "no matching artifacts". A registry such as {"presets":{"p1":[]}} therefore allowed a layout migration even though p1's ownership is unknown, risking deletion of preset-managed files. Now raise _PresetRegistryUnreadableError for a non-dict preset entry, a non-dict registered_commands, or a non-list registered_skills. Extend the unit test to cover these malformed shapes. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@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> |
||
|
|
6d77b4a099 |
fix(integrations): catch OverflowError on a priority: .inf in add/remove (#3589)
IntegrationCatalog.add_catalog and remove_catalog re-validate the
existing catalog entries' priorities inline, separately from the base
loader. Both did `int(raw_priority)` under `except (TypeError,
ValueError)`, so a `priority: .inf` (float('inf')) raised OverflowError:
add_catalog leaked a raw traceback instead of IntegrationValidationError,
and remove_catalog crashed while building the display order.
Add OverflowError to both handlers, matching the base loader (#3525) and
the workflow/step loaders (#3526). add_catalog now raises
IntegrationValidationError; remove_catalog falls back to positional order
like the other non-integer priorities.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
c864fc7447 |
fix(integrations): Forge dispatches hyphenated /speckit-<cmd> invocations (#3529)
Forge installs its slash-commands with hyphenated names (speckit-foo-bar, via format_forge_command_name and the injected frontmatter name), but ForgeIntegration inherited MarkdownIntegration.build_command_invocation, which builds the dotted /speckit.<cmd>. So 'workflow'/command dispatch invoked /speckit.plan while the registered command is /speckit-plan — a name Forge never registered. Override build_command_invocation to reuse format_forge_command_name, producing /speckit-<name> (with '.'-to-'-' for extension commands), mirroring the skills agents' hyphenated invocation. Tests assert Forge core + extension invocations are hyphenated, incl. args (fail before: dotted /speckit.plan / /speckit.git.commit). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
fd101d531e |
feat(integrations): add Grok Build skills-based integration (#3535)
* feat(integrations): add Grok Build skills-based integration Add first-class support for xAI Grok Build via SkillsIntegration, installing speckit skills under .grok/skills and wiring init/invocation/catalog surfaces. Assisted-by: Grok Build (model: grok-4, supervised) * test+docs: address Copilot review on Grok multi-install and next steps Assert init next-steps guidance for Grok (.grok/skills, /speckit-*) and clarify that multi-install safety is path/manifest isolation, not agent-context defaults such as shared AGENTS.md. * fix(integrations): Grok headless --always-approve and isolation paths Document Grok multi-install isolation as .grok/skills and .grok/rules. Override build_exec_args to pass --always-approve so non-interactive dispatch is not blocked at tool permission gates. * docs(integrations): list only managed .grok/skills for Grok isolation Multi-install isolation documents Spec Kit-managed paths; Grok only writes .grok/skills, so drop the read-only .grok/rules entry. * fix(integrations): always-slash Grok hooks and refresh catalog date Move grok to ALWAYS_SLASH_AGENTS so hooks never emit /speckit.plan when ai_skills is missing/false. Update slash-format tests, persist ai_skills on init, and bump catalog updated_at for the Grok entry. --------- Co-authored-by: Nate Chadwick <1232206+natechadwick@users.noreply.github.com> Co-authored-by: test <test@example.com> |
||
|
|
f065e27478 |
test: cover preset constitution seeding through init CLI (#3297)
* Fix preset-constitution-not-installed: use PresetResolver in constitution setup Apply the remediation from the bug assessment on issue #3272. Changes: 1. Modify ensure_constitution_from_template (init.py) to resolve the constitution-template through the preset priority stack via PresetResolver, instead of hardcoding the core template path. This ensures a preset's replacement constitution-template is used when seeding .specify/memory/constitution.md. 2. Reorder init flow: move ensure_constitution_from_template to after the preset installation block so that 'specify init --preset' seeds the memory file from the already-resolved template stack, not from the generic template that existed before the preset arrived. 3. Add _maybe_reseed_constitution to PresetManager (presets/__init__.py): a post-install hook that re-seeds .specify/memory/constitution.md from the preset's constitution-template during 'specify preset add' on an existing project, but only when the memory file still contains generic placeholder tokens ([PROJECT_NAME] or [PRINCIPLE_1_NAME]). Legitimately authored constitutions (no placeholder tokens) are never overwritten. 4. Add regression tests covering both code paths (TestConstitutionReseedOnPresetInstall and TestEnsureConstitutionFromTemplate in tests/test_presets.py). Refs #3272 Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Harden preset constitution resolution Use manifest-aware composed content, atomic safe writes, and conservative generic-template matching for constitution seeding and re-seeding. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 49891a32-bec4-462c-a7f2-6d6ec4eefcdb * Limit preset CLI change to regression test Remove accidental whole-file Ruff formatting introduced during conflict resolution so the PR contains only the intended end-to-end test. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 49891a32-bec4-462c-a7f2-6d6ec4eefcdb * Make preset init test depend on init ordering Disable preset-install lifecycle seeding in the regression test so it fails unless init materializes the constitution after registering the preset. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 49891a32-bec4-462c-a7f2-6d6ec4eefcdb --------- 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: Ben Buttigieg <70525+BenBtg@users.noreply.github.com> |
||
|
|
2fb18c73cb |
fix(integration): preserve ai_skills on use for skills-mode Copilot (#3550) (#3551)
`specify integration use copilot` against a Copilot install configured with `--integration-options "--skills"` dropped `"ai_skills": true` from init-options.json and regenerated extension commands in the legacy `.agent.md`/`.prompt.md` layout, contradicting `integration.json`'s stored `parsed_options.skills: true`. `_update_init_options_for_integration` only inspected `SkillsIntegration` / the instance `_skills_mode` flag. On the `use` path no `setup()` runs, so the freshly-resolved Copilot instance has `_skills_mode == False` and the stored skills intent in `parsed_options` was ignored. Thread the resolved `parsed_options` through and treat `parsed_options["skills"]` as skills mode. Adds a regression test that resets the registry singleton's `_skills_mode` to simulate a fresh process (in-process singleton reuse otherwise masks the bug). Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Copilot-Session: 06fb6ae9-f444-4dfd-ab3f-d0669c5d0604 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> |
||
|
|
353851e966 |
fix(presets): seed constitution from preset constitution-template (#3272) (#3276)
* fix(presets): seed constitution from preset constitution-template (#3272) The constitution is the only template materialized to a live file (.specify/memory/constitution.md) rather than resolved on demand, yet ensure_constitution_from_template hardcoded a copy from the core template and ignored PresetResolver. Combined with init seeding the constitution before preset installation, a preset's constitution-template (e.g. strategy: replace with a ratified constitution) could never go live. Changes: - ensure_constitution_from_template now resolves constitution-template through PresetResolver, so a preset/override/extension wins and core is the fallback. - init seeds the constitution after preset installation so init --preset uses the resolved stack. - install_from_directory re-seeds memory/constitution.md from the resolved preset template, guarded to only act when the memory file is missing or still contains generic placeholder tokens — authored constitutions are never overwritten. Covers preset add and install_from_zip. - Tests for preset seeding, placeholder re-seed, authored-constitution preservation, override resolution, and resolver-aware init seeding. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(presets): compose constitution-template when seeding memory Take on review feedback from Copilot and gglachant: - constitution seeding previously copied the top layer file path verbatim even when the winning layer used a composing strategy (prepend/append/wrap), which could leave {CORE_TEMPLATE} unresolved. - both seeding paths now inspect resolver layers and only copy verbatim for replace; non-replace strategies materialize composed content via PresetResolver.resolve_content(). - add regression tests for wrap strategy composition in both PresetManager seeding and ensure_constitution_from_template. - add a drift-guard test pinning _CONSTITUTION_PLACEHOLDER_TOKENS to the placeholders in templates/constitution-template.md. Assisted-by: GitHub Copilot (model: GPT-5.3-Codex, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * refactor(presets): unify constitution template materialization Address latest Copilot feedback on the constitution seeding path: - moved resolver/layer I/O behind the existing-memory fast path in init - corrected tracker output for composed materialization - deduplicated materialization logic shared by init and preset install seeding into presets._materialize_constitution_template() Behavior is unchanged for replace strategies (copy verbatim) and remains composed for prepend/append/wrap via resolve_content(). Assisted-by: GitHub Copilot (model: GPT-5.3-Codex, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(init): restore shutil import The constitution materialization refactor removed the module import, but init still uses shutil.rmtree when cleaning up a failed new-project initialization. Restore the import so the required ruff check passes. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(presets): harden constitution materialization Address the outstanding review batch for preset constitution seeding: - use checked atomic writes and reject symlinked memory paths - replace placeholder heuristics with hash/source provenance - rematerialize unchanged generated constitutions by resolver priority - preserve authored or edited constitutions, including placeholder mentions - warn non-fatally when post-install materialization cannot complete - retain exact core-template comparison for legacy projects without provenance Add focused provenance, priority, symlink, and failure-path coverage, and update integration inventories for the generated provenance sidecar. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1b2c095d-b45c-4d52-8d56-bd6121d96ab6 * fix(presets): reconcile constitution after removal When the removed preset supplied constitution-template, rematerialize the winning remaining resolver layer only if provenance proves the live file is still generated and unchanged. Preserve edited constitutions and report post-removal reconciliation failures as non-fatal warnings. Add coverage for restoring the core layer, falling back from a removed higher-priority preset, and preserving edited generated content. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1b2c095d-b45c-4d52-8d56-bd6121d96ab6 * fix(presets): tighten legacy constitution provenance Trust only the immutable bundled/source constitution template when migrating legacy projects without provenance. Do not infer core provenance from mutable project templates or preset source labels, including IDs beginning with core. Also detect convention-based constitution-template files before preset removal so unchanged generated constitutions reconcile to the next resolver layer. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1b2c095d-b45c-4d52-8d56-bd6121d96ab6 * fix(presets): preserve files with invalid provenance Use immutable-core legacy migration only when the provenance sidecar is absent. If a sidecar is malformed or its hash does not match the live constitution, treat the file as edited and preserve it. Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1b2c095d-b45c-4d52-8d56-bd6121d96ab6 * fix(presets): reconcile constitution on stack changes Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1b2c095d-b45c-4d52-8d56-bd6121d96ab6 * fix(presets): guard constitution reconciliation edges Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1b2c095d-b45c-4d52-8d56-bd6121d96ab6 --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> |
||
|
|
91839fba50 |
feat(extensions): port git extension scripts to Python (#3400)
* feat(extensions): port git extension scripts to Python Ports git-common, initialize-repo, auto-commit, and create-new-feature-branch to extensions/git/scripts/python/, mirroring the bash/PowerShell twins. Parity tests run each bash script and its Python twin in identical projects and compare output, exit codes, and resulting git state. Fixes #3282 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: match bash error message for whitespace-only descriptions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Handle unreadable git-config.yml and assert stderr parity An unreadable config file raised OSError with a full traceback from _parse_auto_commit_config. Treat it like a missing config: auto-commit stays disabled. Covered by a chmod-000 test (skipped on non-POSIX and as root). _assert_parity now also compares stderr so warning or usage-text regressions between the bash and Python twins fail the suite. All existing parity tests pass with the stricter assertion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(extensions/git): pass script path to core.get_repo_root for cwd-outside-repo callers Without script_file, core.get_repo_root() falls back to Path.cwd() when SPECIFY_INIT_DIR is unset and no .specify root is found upward — the bash twin instead falls back to the script's install location (.specify/scripts/...). Pass script_file so both twins resolve the same repo_root; TypeError fallback keeps older cores working. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: exercise SPECIFY_INIT_DIR from outside the project Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(extensions/git): handle UnicodeDecodeError and USER/USERNAME fallback - Catch (OSError, UnicodeDecodeError) when reading git-config.yml in create_new_feature_branch.py, initialize_repo.py, and auto_commit.py so invalid UTF-8 config falls back to defaults instead of crashing with a traceback. - Fall back to USERNAME (then "unknown") when USER is unset when deriving the branch author token, matching the PowerShell twin's Windows-friendly fallback chain. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(extensions/git): platform-aware persist hint and stronger SPECIFY_INIT_DIR test - Add a shared _persist_hint() helper in create_new_feature_branch.py and use it for both the JSON-mode stderr hint and the human-readable stdout hint, so there is a single place emitting the SPECIFY_FEATURE persistence guidance. On Windows (os.name == "nt") it prints PowerShell $env:VAR = "..." syntax; elsewhere it keeps the existing POSIX export VAR=... syntax (parity with the bash twin). - Rework test_specify_init_dir_resolves_target_project so SPECIFY_INIT_DIR is the only thing that can produce the observed result: the script now runs from a separate host_proj (no existing specs, so script/cwd-based discovery would yield 001) while SPECIFY_INIT_DIR points at a different target_proj that already has an existing spec (007-existing, so the override must yield 008). The old version pointed SPECIFY_INIT_DIR at the same project the script was installed in, so it passed even if the env var were ignored. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(extensions): tolerate missing Git executable Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(extensions): quote PowerShell persist hint Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(git): match bash persist hint escaping Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(git): ignore unterminated config record Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(git): handle Windows persist hint parity Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(init): install Python shared scripts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test(git): normalize Windows persistence hints Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
6ab0c1dac1 |
fix(integrations): escape control characters in goose recipe YAML renderer (#3384)
* fix(integrations): escape control characters in goose recipe YAML renderer YAML forbids C0 control characters (except tab and newline) and DEL in every scalar form, and a bare CR acts as a line break inside a block scalar. _render_yaml wrote the body verbatim into a |2 literal block scalar, so such bodies produced recipes the YAML parser rejects. Detect block-scalar-unsafe characters and fall back to an escaped double-quoted scalar via yaml.safe_dump, mirroring the TOML renderer's fallback strategy from #3341. Fixes #3382 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(integrations): use sys.maxsize instead of float inf for yaml width Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(integrations): extend block-scalar guard to C1 controls and Unicode line breaks YAML's printable set excludes C1 controls (U+0080-U+009F except NEL), and YAML 1.1 treats NEL/LS/PS as line breaks inside a literal block scalar, so bodies carrying any of these still produced unparseable recipes. Widen the fallback guard to the full class and cover it in the regression loop. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(integrations): also treat surrogates and U+FFFE/U+FFFF as block-scalar unsafe YAML's printable set also excludes lone UTF-16 surrogates and the non-characters U+FFFE/U+FFFF; bodies carrying them still hit the literal block path and produced unparseable recipes. Extend the guard and the regression loop. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs(integrations): clarify YAML prompt serialization Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
993083405e |
fix(init): don't block on confirmation for 'init --here' without a TTY (#3236)
* fix(init): don't block on confirmation for 'init --here' without a TTY When 'specify init --here' targets a non-empty directory without --force, it called typer.confirm() unconditionally. In a non-interactive session (no TTY -- CI, piped, agent) there is no input, so the prompt reads EOF and aborts unhelpfully (or blocks), with no actionable message. The named-project path already fails fast and points to --force; --here was the inconsistent outlier. Guard the confirmation with the existing _stdin_is_interactive() helper: when non-interactive, print a clear 'directory not empty; re-run with --force' error and exit 1 instead of prompting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(init): honor piped confirmation for 'init --here'; only fail-fast on empty stdin The first version of this fix short-circuited on '_stdin_is_interactive()' (isatty) before typer.confirm, which broke 'init --here' when confirmation is piped (e.g. 'echo y | specify init --here' / CliRunner input='y\n') -- a non-TTY pipe with valid input was wrongly rejected, regressing test_init_here_without_force_preserves_shared_infra. Instead, call typer.confirm normally (piped 'y'/'n' is honored) and catch the Abort/EOFError it raises only when stdin is empty, converting that to the actionable '--force' guidance. This keeps the UX win for the no-input case without rejecting piped input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(init): distinguish interactive cancel from no-input; defer merge warning Address Copilot review on the --here non-empty path: (1) treat typer.Abort during an interactive confirm (e.g. Ctrl+C) as a normal cancellation (exit 0), and only emit the '--force' guidance + exit 1 when there is no TTY (empty stdin / EOF) -- no longer conflating the two; (2) move the 'will be merged / may overwrite' warning so it only shows when actually proceeding (force) or folded into the confirmation prompt, not on the fail-fast path where nothing is merged. Piped confirmation (e.g. 'echo y | specify init --here') is still honored, which is why the prompt is attempted rather than refused outright when non-interactive -- the existing test_init_here_without_force_preserves_shared_infra pipes 'y' and must succeed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(init): fail fast on non-interactive --here instead of prompting Per Copilot review: do not call typer.confirm when stdin is not a TTY -- an open-but-idle non-TTY stdin (CI/agent) could block on the prompt. When the directory is non-empty and --force is not given, fail fast with '--force' guidance unless an interactive terminal is present. Interactive confirm still offers the merge-but-preserve path (distinct from --force, which overwrites); a Ctrl+C there is treated as a normal cancellation (exit 0). The merge/overwrite warning is only printed when actually proceeding, not on the fail-fast path. Updated the preserve-merge E2E test to simulate an interactive terminal so it exercises the confirm path (non-interactive sessions now require --force). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(init): honor piped y/n for 'init --here', error only on no-input Per maintainer review: restore the second-revision shape. Calling typer.confirm normally keeps 'echo y | specify init --here' reaching the non-destructive preserve-merge path (and piped 'n' cancels with exit 0). Only when no confirmation input is available at all (closed/empty stdin -> typer.Abort/EOFError) is it converted into the actionable error that points at --force. This drops the _stdin_is_interactive fail-fast that broke the common piped-confirm idiom and made preserve-merge interactive-only. The preserve test no longer needs to monkeypatch _stdin_is_interactive - it passes on the real contract. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(init): preserve interactive-cancel semantics; fold merge risk into the prompt Two review-driven refinements to the 'init --here' non-empty confirm, keeping the maintainer-endorsed control flow (piped y/n honored; non-interactive EOF → actionable --force error): 1. typer.confirm raises typer.Abort for BOTH an interactive Ctrl+C and an EOF on closed/empty stdin. Catching it unconditionally reported 'no confirmation input available, use --force' and exited 1 even when the user cancelled at a real TTY. Branch on _stdin_is_interactive(): a TTY cancel is a normal exit 0 ('Operation cancelled'); only non-interactive EOF becomes the --force error. 2. Fold the merge-risk warning into the confirmation question instead of printing it unconditionally beforehand, so the EOF/no-input path (which exits without changing anything) no longer prints a misleading 'will be merged' line first. Adds test_init_here_interactive_cancel_exits_zero (fails before: exit 1 with --force; passes after: exit 0, 'cancelled', pre-existing file untouched). The non-interactive EOF and piped-y preserve-merge tests are unchanged and still pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c05a626cbc |
fix(integrations): exit cleanly on unbalanced quote in --integration-options (#3457) (#3466)
* fix(integrations): exit cleanly on unbalanced quote in --integration-options (#3457) `_parse_integration_options` called `shlex.split(raw_options)` unguarded, so an unbalanced quote in the flag value (e.g. `--integration-options='--commands-dir "foo'`) made shlex raise `ValueError: No closing quotation` and a raw traceback escaped — unlike every other bad-input path in this function (unknown option, missing value, unexpected value), which print a message and exit 1. Reachable from `specify init --integration-options=...` and every `specify integration install/switch/upgrade/migrate --integration-options=...`. Wrap the split in a try/except ValueError that prints a one-line error and raises `typer.Exit(1)`, matching the existing loud-fail UX. Add a test asserting the unbalanced-quote input raises `typer.Exit` with exit code 1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
0acb5c6461 |
fix(integrations): declare kiro-cli multi-install safe (#3471) (#3485)
kiro-cli confines all of its managed files to an isolated agent root
(`.kiro/`, with commands in `.kiro/prompts`) that no other integration
writes to, so it meets every documented criterion for multi-install
safety — but `KiroCliIntegration` never set `multi_install_safe = True`.
As a result, co-installing kiro-cli alongside any other integration left
`specify integration status` permanently in ERROR:
error unsafe-multi-install: Installed integrations are not all
declared multi-install safe: kiro-cli
`--force` bypasses the install-time gate but does not clear the status
error, and there is no flag or config to acknowledge it, so the error is
permanent while both integrations remain installed.
Set `multi_install_safe = True`. The registry's parametrized
multi-install-safe contract tests (static isolated root, distinct agent
roots / command dirs, disjoint manifests) now cover kiro-cli
automatically, and a focused regression test pins the declaration so a
future edit cannot silently drop it and reintroduce the error.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
6664cf813c |
fix: mark Kiro integration as multi-install safe (#3472)
Assisted-by: Codex (model: GPT-5, autonomous) |
||
|
|
14fa3ada08 |
fix(catalogs): raise catalog error, not raw ValueError, on a malformed URL (#3435)
CatalogStackBase._validate_catalog_url read parsed.hostname, which raises ValueError on a malformed authority (e.g. an unclosed ipv6 bracket "https://[::1"). every other reject path raises the class catalog error, so the raw ValueError leaked to the caller. wrap the parse + hostname access and convert ValueError to the normal error via cls._error. twin of the bundler fix in adapters._validate_remote_url. |
||
|
|
c8ce488073 |
chore: add pre-commit config and fix trailing whitespace/end-of-file (#3430)
issues |
||
|
|
15ac745e8d |
fix(integrations): skip Windows Store python3 alias stub in resolve_python_interpreter (#3385)
* fix(integrations): skip Windows Store python3 alias stub in resolve_python_interpreter
On stock Windows, python3 on PATH is the Microsoft Store App Execution
Alias stub: it exists but only prints an installer hint and exits
non-zero, so generated {SCRIPT} invocations for the py script type were
broken. Verify the found interpreter actually runs before accepting it,
on Windows only, mirroring the parse-success-not-availability approach
of #3312/#3320 for the sh scripts. POSIX keeps the plain existence
check. sys.executable remains the fallback and is always live.
Fixes #3383
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(integrations): probe interpreter isolated and without site, discard I/O
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: pin POSIX platform in PATH-resolution tests
The tests fake shutil.which with POSIX paths; on Windows CI the real
sys.platform made the stub probe run against those fake paths and
fall through to sys.executable.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
||
|
|
8e2e2d2f25 |
fix(integrations): escape control characters in SKILL.md frontmatter (#3399)
yaml.safe_dump with default_style='"' replaces the hand-rolled quote helpers in base.py and hermes, so newlines and control characters in template descriptions round-trip instead of producing unparseable YAML. Fixes #3391 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |
||
|
|
892dd656f2 |
fix(shared-infra): refresh_shared_templates preserves recovered user files (#3378)
* fix(shared-infra): refresh_shared_templates preserves recovered user files refresh_shared_templates skipped a shared template only when it was untracked or modified, ignoring the manifest's is_recovered marker that install_shared_infra already honors. So a pre-existing user template (adopted via record_existing(recovered=True), hence tracked and hash-unmodified) was silently overwritten with bundled content on refresh — the exact data-loss class that #2918 fixed for install_shared_infra. Add the is_recovered check to the skip predicate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(shared-infra): include recovered files in refresh skip warning The skip predicate now also skips recovered (pre-existing user) files, so the warning saying only 'modified or untracked' could mislead a user into thinking they edited a file that was simply recorded as recovered. Reword to 'modified, untracked, or preserved (recovered)'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
54ed736479 |
fix(agents): resolve skill placeholders in Goose (yaml) command output (#3374)
* fix(agents): resolve skill placeholders in Goose (yaml) command output
CommandRegistrar.register_commands resolves {SCRIPT}/__AGENT__ and the
$ARGUMENTS placeholder in the markdown and toml branches, but the yaml
branch (Goose recipes) called render_yaml_command directly, skipping
both. So extension/preset command bodies installed for Goose kept literal
{SCRIPT}, __AGENT__, and repo-relative script paths in the generated
.goose/recipes/*.yaml prompt. Mirror the markdown/toml branches: run
resolve_skill_placeholders + _convert_argument_placeholder on the body
before render_yaml_command.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(goose): assert positive placeholder replacements in recipe prompt
Per review: parse the generated recipe with yaml.safe_load and assert the
prompt contains the resolved values (.specify/scripts/, 'agent goose',
{{args}}), not merely that the literal tokens are absent — a wrong output
that happens to omit the exact strings would otherwise pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
643f73a1d7 |
test: isolate integration test home (#3144)
* test: isolate integration test home Assisted-by: Codex (model: GPT-5, autonomous) * test: assert integration home isolation Assisted-by: Codex (model: GPT-5, autonomous) * test: extend integration home isolation to module-scoped setup Address Copilot review on #3144. Add a session-scoped autouse fixture so HOME/USERPROFILE/XDG are redirected for setup that runs outside a test function (e.g. the module-scoped status_* fixtures in test_integration_subcommand.py that run `specify init` before any per-test isolation applies). The function-scoped fixture still overrides HOME per test. Also assert Path.home() resolves to the isolated home, since most integrations (Hermes, catalog) read the home via that API rather than the env vars directly. |
||
|
|
94c7ec288f |
fix(toml): escape control characters so generated command files parse (#3341)
* fix(toml): escape control characters so generated command files parse both toml renderers (TomlIntegration._render_toml_string for gemini/tabnine and CommandRegistrar.render_toml_command for extension/preset commands) wrote control characters raw into a multiline or basic string. toml forbids literal control chars (U+0000-U+001F except tab/newline, and U+007F) in every string form, and a bare CR that is not part of a CRLF pair, so a description or body containing one produced a .toml file that fails to parse. route any value with such a character to a fully-escaped basic string that emits the leftover control chars as \uXXXX. added regression tests that round-trip through tomllib. * refactor(toml): centralize control-char escaping in one shared helper the control-char detection and basic-string escaping added for both toml renderers were copy-pasted into agents.py and integrations/base.py. move the two functions into specify_cli/_toml_string.py and have both renderers delegate to it, so the escaping rules can't drift apart later. no behavior change; both renderers now reference the same implementation. |
||
|
|
10d4bca64c |
fix(integrations): guard _sha256 against unreadable managed files (#3376)
manifest.py::_sha256 does an unguarded open(). check_modified() and uninstall() both call it on a readable-but-unopenable regular file (e.g. permission denied) without catching OSError, so 'specify integration upgrade/uninstall/switch' surface a raw PermissionError traceback. Guard both call sites: in check_modified() treat an unreadable file as modified (consistent with the adjacent symlink / non-regular-file handling); in uninstall() treat it as skipped and preserve it (mirroring the existing path.unlink() OSError guard just below). The force short-circuit is unchanged. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |