Commit Graph

1612 Commits

Author SHA1 Message Date
21Silva
88e997306f docs: add Simplified Chinese translation of README (#3740)
Add README.zh-CN.md with a hand-crafted (non-machine) Chinese
translation of the project README, and add a language switcher
link at the top of both README files.

Code blocks, command names, badges, and links are kept identical to
the English source; only prose is translated.

Co-authored-by: yifosheng001 <yifosheng001@ke.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 13:59:39 -05:00
github-actions[bot]
186ca25c99 Update Intake Sequencing Governance preset to v0.2.2 (#3809)
Update intake-sequencing-governance preset submitted by @hindermath to:
- presets/catalog.community.json (version, download_url, documentation, description, provides)
- docs/community/presets.md community presets table

Closes #3807

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-28 13:18:10 -05:00
Ali jawwad
a9c9905400 fix(workflows): reject non-string/non-boolean 'condition' in if/while/do-while steps (#3706)
* fix(workflows): reject non-string 'condition' in if/while/do-while steps

`if_then`, `while_loop`, and `do_while` validate() confirm `condition` is
present but never that it is a string. execute() feeds it to
`evaluate_condition()`, which returns a non-string as-is and takes `bool()`
of it -- so `condition: [1, 2]` (a list authoring mistake) silently resolves
to `True`, branching wrongly / spinning the loop to `max_iterations`, with no
error reported.

Reject a present-but-non-string `condition` at validation, mirroring the
existing prompt/shell/command 'must be a string' guards. `"true"`/`"false"`
and expressions like `"{{ ... }}"` are strings, so they stay valid.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* docs(workflows): describe the evaluate_expression/evaluate_condition split accurately

Address review feedback: the guard comments attributed the non-string
pass-through to evaluate_condition(), which always returns a bool. It is
evaluate_expression() (called by evaluate_condition) that returns a non-string
unchanged; evaluate_condition then applies bool() to that value.

Reword all four sites (if/while/do-while guards + the mirror test comment) to
name the two stages correctly. Comments only -- no behaviour change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* fix(workflows): keep a literal bool 'condition' valid

Self-review catch: the guard rejected EVERY non-string, which broke an input
that previously worked. An unquoted ``condition: false`` is idiomatic YAML and
resolves exactly today -- evaluate_expression passes the bool through and
evaluate_condition's bool() is a no-op (verified: evaluate_condition(False) is
False, (True) is True). The if/while steps even default ``condition`` to the
bool ``False`` themselves, so bool is the field's natural type, not an
authoring mistake.

Accept (str, bool) and reject only the genuinely silent-coercion types
(list/dict/int/float, e.g. condition: [1, 2] is always True). Message updated
to "must be a string or boolean"; the bad-value tests drop True and gain 1.5,
and each step gains a positive bool case.

🤖 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>
2026-07-28 11:40:36 -05:00
Marsel Safin
054fb7723d fix(bundle): escape catalog metadata in discovery output (#3774)
* fix(bundle): 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(bundle): escape provides fallback values

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>
2026-07-28 11:35:49 -05:00
Noor ul ain
8bfe6e14d1 fix(workflows,extensions): tolerate non-list catalog tags in search/info display (#3770)
`workflow search`, `workflow info`, `extension search` and `extension info`
crashed with `TypeError: 'int' object is not iterable` when a catalog entry
carried a scalar `tags:` value (e.g. `tags: 5`). Catalog payloads are
user-editable YAML/JSON, so this shape reaches the display unvalidated.

Both backends already guard their tag *filter* with
`isinstance(raw_tags, list)` — `WorkflowCatalog.search` and
`ExtensionCatalog.search` skip a non-list `tags` cleanly. Only the display
paths were unguarded: they tested truthiness (`if info.get("tags"):`) and
then iterated. A scalar is truthy but not iterable, so `--tag` filtering
survived while plain `search`/`info` rendering blew up.

Note this is distinct from the non-string *element* handling added in
#3746/#3747: coercing elements with `str(t) for t in ...` does not help when
`tags` is not a sequence at all. The fix is the guard the sibling
integration commands already use — `integrations/_query_commands.py:332,402`
gate on `isinstance(tags, list) and tags`. This aligns workflows and
extensions with that reference pattern, leaving all four tag-join display
sites in these modules consistent.

Regression tests drive the full CLI via CliRunner and cover search + info in
one case per module; both fail before the fix with the exact TypeError.


Assisted-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 11:24:19 -05:00
Marsel Safin
a482fb2fce fix: correct nullable resolved directory annotation (#3771)
Assisted-by: GitHub Copilot (model: GPT-5.6 Sol, autonomous)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-28 11:23:02 -05:00
Noor ul ain
05cb3cba34 fix(presets): tolerate non-string and non-list catalog fields in preset search/info (#3769)
* fix(presets): tolerate non-string and non-list catalog fields in preset search/info

`preset search` and `preset info` crashed with a raw traceback on catalog
payloads that are valid YAML/JSON but not string-typed. Catalog files are
user-editable, so these shapes reach the code unvalidated.

`PresetCatalog.search` had three unguarded assumptions:

- `--author` called `.lower()` on the raw value → `AttributeError: 'int'
  object has no attribute 'lower'` for `author: 789`.
- the query searchable-text join passed raw `name`/`description` through →
  `TypeError: sequence item 0: expected str instance, int found`.
- the `--tag` filter iterated `tags` without a list check, so a scalar
  `tags: 5` (truthy, not iterable) raised `TypeError: 'int' object is not
  iterable`.

PR #3743 fixed only the non-string *elements* of `tags` here; a non-list
`tags` and the `author`/`name`/`description` fields were still unguarded.
The sibling catalogs already handle all of these — `extensions/__init__.py`
and `integrations/catalog.py` coerce with `str(...)` and gate on
`isinstance(raw_tags, list)`. This aligns presets with them.

The same scalar-`tags` crash reached the four display sites in
`presets/_commands.py`, so those now gate on `isinstance(tags, list)`,
matching `integrations/_query_commands.py`. Note `PresetManifest.tags`
returns `self.data.get("tags", [])` and manifest validation does not
enforce list-ness, so a local `preset.yaml` with `tags: 5` validates
successfully and then crashed `preset info` — hence the guard on the
local-manifest branch too.

While here, `preset search` printed tags unescaped, so a tag containing
`[bold]` was silently swallowed as a Rich style tag; it now routes through
`_escape_markup` like the `preset list` line directly above it.

Regression tests in `TestPresetTagsNonString` drive the full CLI path via
CliRunner. All five fail before the fix, each with the exact exception it
targets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Assisted-by: Claude Opus 5 (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>

* chore: regenerate security audit requirements (annotated-doc 0.0.5)

The Security Audit workflow's "Check committed audit requirements are
current" step regenerates requirements with `uv pip compile --upgrade`,
which now resolves annotated-doc==0.0.5. Re-sync the committed snapshot
so the check passes. No pyproject dependency changes; upgrade drift only.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b3d0333b-4a36-4f69-9273-3ac0c3f46481

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Copilot-Session: b3d0333b-4a36-4f69-9273-3ac0c3f46481
2026-07-28 11:07:51 -05:00
Marsel Safin
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>
2026-07-28 10:20:31 -05:00
github-actions[bot]
999f8e6497 Update Verify Review Ship extension to v0.4.2 (#3792)
Update verify-review-ship extension submitted by @cadugevaerd to:
- extensions/catalog.community.json (version, download_url, sha256, updated_at)
- docs/community/extensions.md community extensions table

Closes #3791

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-28 10:20:07 -05:00
Ben Buttigieg
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
2026-07-28 15:48:40 +01:00
github-actions[bot]
809b4c5e26 Update Intake Review Governance preset to v0.2.0 (#3796)
Update intake-review-governance preset submitted by @hindermath:
- presets/catalog.community.json (version, download_url, documentation, description, scripts, tags)
- docs/community/presets.md community presets table

Closes #3794

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-28 09:37:09 -05:00
github-actions[bot]
1354eade99 fix(constitution): stop propagating guidance into templates (#3737) (#3790)
Issue #3737 asked the /constitution command to synchronize constitutional
guidance into every effective task/plan/spec template, including active
preset-provided replacements. This changes the fix's direction: rather than
teach the command to discover and edit more template layers, it removes the
template-propagation behavior entirely.

Why this is the correct fix:

- The governed templates do not embed constitutional content. plan-template
  carries a runtime placeholder ("[Gates determined based on constitution
  file]") and spec-template/tasks-template reference no principles at all.
- The consuming commands read .specify/memory/constitution.md at runtime and
  derive their Constitution Check gates live (plan, tasks), and analyze is the
  dedicated drift checker that validates spec/plan/tasks against the
  constitution. Enforcement is therefore already automatic and always current.
- Statically editing template files fights the preset/override composition
  system: a replace preset shadows an edited core template entirely, and a
  hand-edited versioned preset file is clobbered on its next update. Presets and
  extensions are formalized, versioned artifacts the command must not mutate.

So the original bug (constitution edits missing active preset templates) is
resolved by not propagating at all: the runtime read is the single source of
truth. The /constitution command is scoped to its own artifact — it drafts and
writes the constitution and reports a Sync Impact Report changelog, and no
longer reads, edits, or reports on plan/spec/tasks/preset/extension templates.

Refs #3737

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5b80589c-74e8-42e5-b2cb-7a7e0d69a964
2026-07-28 09:17:32 -05:00
Manfred Riem
2a29b534ae chore: release 0.14.3, begin 0.14.4.dev0 development (#3795)
* chore: bump version to 0.14.3

* chore: begin 0.14.4.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-28 09:15:50 -05:00
github-actions[bot]
d8d62756fe Update Intake Authoring Governance preset to v0.3.0 (#3788)
Update intake-authoring-governance preset submitted by @hindermath to:
- presets/catalog.community.json (version, download_url, documentation, description, provides, tags, updated_at)
- docs/community/presets.md community presets table

Closes #3780

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-28 08:34:17 -05:00
Faqeha Noor
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>
2026-07-28 08:26:12 -05:00
orize
39f2ac3c63 clarify: require real interrogatives, ban topic-label questions (#3745)
* clarify: require real interrogatives, ban topic-label questions

Agents often present topic labels or bare requirement ids as "questions",
which are not answerable on their own. Require a full interrogative under
**Question:**, a plain-language stake sentence, then Recommended/options.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update templates/commands/clarify.md

* clarify: allow requirement ids only after the ?

Resolves Copilot feedback: an interrogative ending in ? cannot also have
a parenthesized id "at the end of the question." Exact format is now
`**Question:** <interrogative>?` or `**Question:** <interrogative>? (FR-023)`.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Lorin O'Brien <lorin@pronto.net>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 07:58:12 -05:00
Eric X Engstfeld
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>
2026-07-28 07:54:13 -05:00
Pascal THUET
118062eac4 harden: secure extension and preset archive downloads (#3141)
* harden: secure extension and preset archive downloads

Adopt the shared download-security primitives from #3140 across extension and
preset catalog, package, direct-URL, and ZIP-install flows:

- bound catalog, package, and inline manifest reads;
- verify catalog SHA-256 values when present;
- replace path-only extraction with bounded traversal/symlink-safe extraction;
- validate malformed hosts and ports before opening download URLs;
- handle normalized trailing-backslash directory entries consistently.

Redirect enforcement and checksum verification remain owned by the shared
helpers already on main; this commit wires them into extension and preset
behavior.

Assisted-by: OpenAI Codex (model: GPT-5, autonomous)

* harden: close archive and catalog download edge cases

Preflight ZIP central directories before ZipFile allocates them, bound both
declared and actual payload sizes, and reject ambiguous or non-portable archive
paths before extraction.

Keep extension update manifest selection consistent with extraction, reject
unsafe catalog-derived output filenames and malformed URL types, and escape
untrusted values in download errors.

Add regression coverage for parser differentials, collisions, platform-specific
filenames, bounded call sites, and failure ordering.

Assisted-by: OpenAI Codex (model: GPT-5, autonomous)

* harden: address download security review feedback

Assisted-by: OpenAI Codex (model: GPT-5, autonomous)

* harden: close ZIP preflight review gaps

Assisted-by: OpenAI Codex (model: GPT-5, autonomous)

* fix: harden extension update preflight and rollback

Assisted-by: OpenAI Codex (model: GPT-5, autonomous)

* fix: harden extension update rollback

Assisted-by: OpenAI Codex (model: GPT-5, autonomous)
2026-07-28 07:52:07 -05:00
Quratulain-bilal
0117a7b977 fix: correct Optional type annotation for context_note parameter (#3765)
The context_note parameter in CommandRegistrar methods was annotated as
\str = None\ which is a type lie — the default is None but the type
hint says str. Static type checkers (mypy/pyright) would flag this as
an error. Changed to \Optional[str] = None\ for correctness, consistent
with how extension_id (same class) is already typed.
2026-07-27 16:12:20 -05:00
Noor ul ain
2355fcb350 Update AGENTS.md (#2626)
* docs: layer contributor-onboarding sections onto AGENTS.md

Rebased onto current main and reworked so the additions match the
current architecture rather than the stale base this branch was written
against. The original revision documented the retired Windsurf
integration and a CLI-managed `context_file` field that no longer
exists (context files are now owned by the opt-in agent-context
extension), and described the manifest at the wrong path with a
non-existent API.

This version keeps all current AGENTS.md content unchanged and adds four
onboarding-focused sections, verified against the code:

- Quickstart — Add a New Integration in 5 Steps (links into the existing
  step-by-step section; notes context files are extension-owned)
- IntegrationManifest — File Tracking (correct path
  .specify/integrations/<key>.manifest.json and real API:
  record_file / record_existing / hash-guarded uninstall)
- Error Handling and Debugging (symptom/cause/fix table + debug tips)
- Contribution Checklist

Purely additive (+88 lines, no deletions); all internal anchors resolve.

Assisted-by: Claude Opus 4.8 (model: claude-opus-4-8, autonomous)
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>

* 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>
2026-07-27 15:17:38 -05:00
Noor ul ain
98c9e67ce2 fix(extensions): tolerate non-string catalog name in display-name lookup (#3747)
* fix(extensions): tolerate non-string catalog name in display-name lookup

_resolve_catalog_extension() filters catalog search results by display
name with `ext["name"].lower() == argument.lower()`. Extension catalog
JSON is user-editable, so a hand-authored non-string name (e.g.
`name: 123`) crashes the filter with `AttributeError: 'int' object has
no attribute 'lower'`, taking down `extension info <name>` and
`extension add <name>`. A missing `name` key would likewise KeyError.

Coerce defensively with `str(ext.get("name", "")).lower()`, matching the
ambiguous-match display block just below (which already str()-coerces
name for the same reason). A bad-named entry simply doesn't match,
yielding a clean not-found error instead of a traceback.

Adds a regression test invoking `extension info <name>` against a
mocked catalog whose search result has `name: 123`; it fails pre-fix
with AttributeError.

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>

* 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>
2026-07-27 15:00:26 -05:00
Noor ul ain
6136706ef3 fix(presets): coerce non-string catalog tags before joining (#3743)
* fix(presets): coerce non-string catalog tags before joining

Preset catalog payloads are user-editable YAML/JSON, so a `tags:` list
can legitimately contain non-strings (e.g. numeric tags). The preset
list/search/info display paths and the catalog search backend joined
tags with a raw `", ".join(...)` / used `t.lower()`, which raised
`TypeError: sequence item N: expected str instance, int found` (or
`AttributeError` on `.lower()`) and crashed the command.

Sibling command surfaces already guard this — extensions, integrations,
and workflows coerce with `str(t) for t in ...`. This aligns presets:

- `_commands.py`: `preset list`, `preset search`, and both `preset info`
  branches now join `str(t) for t in ...`.
- `__init__.py` `PresetCatalog.search`: tag filter uses `str(t).lower()`
  and the searchable-text join coerces tags to `str`.

Adds regression tests driving `preset search` and `preset info` through
CliRunner with numeric tags; both fail before the fix with the TypeError.

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>
2026-07-27 14:14:59 -05:00
Marsel Safin
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 2486c08, all in the presets/extensions single-active integration
rule (#2948):

1. presets: _reconcile_composed_commands (run after install/remove)
   bypassed the active-only filter entirely, writing composition-winner
   command files for every detected non-skill agent via
   register_commands_for_non_skill_agents. Added an only_agent param to
   that registrar method (mirroring register_commands_for_all_agents)
   and threaded it through all 5 reconciliation call sites.

2. presets: `integration use copilot` with --skills (ai_skills: true)
   wrote both the static .agent.md command file AND the SKILL.md
   mirror for the same override. Mirrored the extension path's
   ai_skills guard in both _register_commands and the reconciliation
   pass: a command-backed active agent running in skills mode is
   excluded from non-skill command registration.

3. presets: registered_skills was a flat list, so switching between
   two skill-mode agents (e.g. Claude -> Codex) and then removing the
   preset only restored the currently active agent's directory,
   permanently orphaning the other. _unregister_skills now restores
   every existing skill-mode agent directory instead of only the
   active one.

4. extensions: load_init_options() collapses "no file" and "corrupted
   file" into the same {}, so the round-2 fail-closed fix didn't
   actually distinguish them. Added a shared
   resolve_active_agent_for_registration() helper in _init_options.py
   that checks file existence separately from parse success, returning
   a distinct sentinel for "file absent" vs None for "corrupted or
   invalid". extensions/__init__.py now uses this helper.

5. presets: same corruption-collapsing bug in _register_commands's
   active_agent resolution. Now uses the same shared helper as (4).

Adds regression tests for all five: reconciliation active-only
filtering, copilot --skills dual-write prevention, multi-skill-agent
switch+remove, and corrupted init-options fail-closed behavior for
both extension add and preset add. Each test was verified to fail
against the pre-fix code and pass with the fix.

Targeted (883) and full (3923 passed, 109 skipped) suites pass; ruff
check clean.

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

* fix: address fourth round of review feedback (skill registration provenance)

Replace the "enumerate every skill-mode directory and restore all of them"
approach from the previous round with precise per-agent provenance
tracking, per reviewer feedback that the enumerate-and-restore-everything
design was unsound:

- registered_skills changes from a flat List[str] to Dict[str, List[str]]
  (agent name -> skill names actually written), mirroring the shape
  registered_commands already uses. _register_skills now returns this
  per-agent mapping instead of a bare list, and every call site
  (register_enabled_presets_for_agent, install_from_directory, the
  _reconcile_skills "was this skill previously managed" check) is updated
  to read/merge the new shape. Legacy flat-list registry entries from
  before this change are still readable: writes self-migrate the format,
  and _normalize_registered_skills() handles the transitional read paths.

- _unregister_skills now restores exactly the agent directories recorded
  for a preset instead of guessing at every skill-mode integration that
  happens to exist on disk. This fixes two problems with the old
  enumerate-everything design: (1) it could silently overwrite or delete
  another preset's (or a user's) override in an agent directory the
  current preset never actually touched, and (2) it depended on
  transient per-process integration state (_skills_mode), which is unset
  in a fresh CLI invocation for mode-selectable integrations like Copilot
  --skills, permanently orphaning their overrides after a process
  restart. Registries written before this change (flat list, no agent
  provenance) fall back to best-effort restoration under only the
  currently active agent, matching the pre-existing guarantee level.

- Every directory resolved from persisted provenance is now validated
  through the project's shared symlink/containment guard
  (_ensure_safe_shared_directory) before any file in it is read, written,
  or removed, since restoration may target an agent that isn't currently
  active and its directory can't be assumed safe just because a name was
  recorded for it.

- _tracked_skill_agent_dirs() (the enumeration helper introduced last
  round) is removed; it's superseded by the provenance-based design.

Adds regression tests: a symlinked skills directory is rejected during
removal; removing one preset does not disturb a different preset's
override in another agent's directory; and a Copilot --skills
registration installed, then removed after switching agents in a fresh
PresetManager instance (simulating a new process), is still correctly
restored. Updates existing skill-registration assertions across
test_presets.py and test_integration_claude.py for the new per-agent
registry shape.

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

* fix: address fifth round of review feedback (symlink presence, rescaffold reconciliation, shared skills dir)

- _init_options.py: resolve_active_agent_for_registration() now treats a
  dangling init-options.json symlink as present (path.is_symlink() check
  alongside path.exists()), since Path.exists() follows symlinks and
  returns False for a broken one. Previously a broken symlink fell back
  to the legacy "no file" path and registered every detected agent
  instead of failing closed.
- presets/__init__.py (register_enabled_presets_for_agent): the
  integration use/switch rescaffold path now collects affected command
  names across all presets processed and runs
  _reconcile_composed_commands/_reconcile_skills once after the loop,
  matching install/remove. Previously rescaffolding wrote each preset's
  raw content directly with no follow-up reconciliation, so a
  project-level override (the highest-priority layer) could be clobbered
  by a lower-precedence preset after switching agents.
- presets/__init__.py (_unregister_skills): multiple integrations can
  share one physical skills directory (agy/codex/zed all resolve to
  .agents/skills). Provenance restoration now groups recorded agent
  entries by resolved directory and restores each physical directory
  exactly once, preferring the currently active agent's renderer when it
  owns that directory (otherwise any recorded owner, chosen
  deterministically). Previously each recorded agent key triggered its
  own restore pass against the same directory, with whichever agent was
  iterated last silently winning regardless of which agent was active.

Adds regression tests for each: a dangling init-options.json symlink
failing closed for both preset resolution and extension add; integration
use rescaffold preserving a project override over a lower-priority
preset; and a codex/agy shared-directory removal restoring the directory
exactly once in the active agent's format.

Targeted (tests/integrations/test_integration_subcommand.py,
tests/test_presets.py, tests/test_extensions.py,
tests/test_extension_skills.py,
tests/integrations/test_integration_opencode.py,
tests/integrations/test_integration_claude.py): 930 passed.
Full suite: 3930 passed, 109 skipped.
ruff check: clean on files touched by this change.

Refs #2948

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

* fix: guard skill subdirectories and active-agent scoping in preset reconciliation

Fix 4 issues from round-6 review of the active-only integration
registration work (#2948):

- remove(): removed_cmd_names only collected primary command names from
  registered_commands + manifest aliases, missing commands that were
  only ever registered via skills mode (ai_skills guard returns no
  command names for command-backed integrations in skills mode). This
  skipped reconciliation entirely when removing a higher-priority
  skills-mode preset, causing _unregister_skills() to fall back to
  core/extension content instead of the surviving lower-priority
  preset's override. Now every command template's primary name is
  added to removed_cmd_names unconditionally.

- _reconcile_composed_commands(): the "composed is None" branch (fires
  when no replace-strategy layer remains for a command, e.g. after
  removing a wrap/append preset's base) called unregister_commands()
  across every configured non-skill agent, ignoring only_agent. This
  deleted historical artifacts from integrations that were never active
  for the preset. Now filtered by only_agent like the rest of the file.

- Added _validate_skill_subdir() helper (reusing
  _ensure_safe_shared_directory/_validate_safe_shared_directory from
  shared_infra.py) and applied it at every site that reads or writes an
  individual skill subdirectory (_register_skills,
  _unregister_skills_in_dir, _reconcile_skills' override_skills
  restoration loop). _safe_skills_dir_for_agent only validated the
  parent skills directory; a symlinked leaf subdirectory (e.g.
  .claude/skills/speckit-specify) would slip past that check since
  is_dir()/exists() follow symlinks, letting write_text/rmtree operate
  through it to an arbitrary location outside the project.

Added regression tests: removing a higher-priority skills-only preset
restores the surviving lower-priority preset's content; composed-is-None
unregistration only touches the active agent; symlinked skill subdirectory
rejected on restore; symlinked skill subdirectory rejected on write.

Targeted (934) and full (3934 passed, 109 skipped) test suites and ruff
check pass clean.

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

* fix: persist command registration before fallible skills phase on rescaffold

Fix remaining round-6 review findings on the active-only integration
registration work (#2948):

- register_enabled_presets_for_agent(): registered_commands and
  registered_skills were merged and persisted together in a single
  registry.update() call after both the commands and skills phases ran.
  If _register_skills() raised, the per-preset try/except swallowed it
  before that update() call was reached, even though _register_commands()
  had already written a real command file to disk. That file became
  untracked, so preset removal could no longer clean it up.
  install_from_directory() already persists registered_commands
  immediately after the commands phase, before starting the independently
  fallible skills phase; rescaffold now does the same.

- test_presets.py: renamed a misleading claude_dir variable (pointing at
  Gemini's command directory) in
  test_composed_none_unregister_respects_active_agent to reuse the
  existing gemini_commands_dir variable already defined earlier in the
  same test.

Added regression test
test_rescaffold_persists_commands_before_fallible_skills_phase:
simulates a skills-phase failure during rescaffold and asserts the
command file already written to disk is still tracked in
registered_commands.

Verified all other round-6 findings (preset active-integration scoping,
preset reconciliation/remove paths, skills-mode switching, override
precedence during rescaffold, skill-subdirectory symlink safety) are
already addressed by prior commits in this branch; re-checked each
against current code before concluding no further change was needed.

Targeted (tests/test_presets.py, tests/test_extensions.py: 689 passed)
and full (3935 passed, 109 skipped) suites and ruff check on changed
files pass clean.

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

* fix: unregister stale opposite-mode preset artifact on same-agent skills toggle

Fix an Important gap in register_enabled_presets_for_agent() surfaced by
quality review (#2948): toggling ai_skills for the *same already-active*
command-backed agent (e.g. `integration upgrade copilot` after flipping
ai_skills, with copilot staying active throughout) left a stale artifact
from the previous mode behind, violating the command/skill mutual-
exclusion invariant this PR otherwise enforces.

- command -> skills: _register_commands()'s ai_skills guard makes the
  commands phase a no-op, but the previously-written command file (e.g.
  .agent.md) and its registered_commands[agent] entry were never cleaned
  up, so it lingered alongside the newly written SKILL.md.
- skills -> command: _get_skills_dir() stops resolving a skills directory
  once ai_skills is off, making the skills phase a no-op, but the
  previously-written SKILL.md and its registered_skills[agent] entry were
  never cleaned up, so it lingered alongside the newly (re)written command
  file.

register_enabled_presets_for_agent() now resolves once per call whether
agent_name is a command-backed integration (extension != "/SKILL.md") and
the current ai_skills state, then narrowly unregisters the stale opposite-
mode entry for that agent via the existing _unregister_commands /
_unregister_skills helpers before persisting updated tracking — mirroring
the same per-agent, per-preset isolation already used elsewhere in this
method. Native skill-only agents (claude, codex, ...) are unaffected:
they have no command/skill toggle, so registered_commands and
registered_skills legitimately co-exist for them by design. The trailing
reconciliation pass, project-override precedence, and per-preset
partial-failure isolation are all unchanged.

Added red-first regression tests exercising the real install +
register_enabled_presets_for_agent rescaffold path in both toggle
directions:
- test_rescaffold_toggle_command_to_skills_removes_stale_command_file
- test_rescaffold_toggle_skills_to_command_removes_stale_skill_file

Both failed against the prior code (stale artifact persisted / registry
still tracked it) and pass after the fix.

Targeted (tests/test_presets.py, tests/test_extensions.py: 691 passed)
and full (3937 passed, 109 skipped) suites and ruff check on changed
files pass clean.

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

* fix: migrate legacy flat-list registered_skills on rescaffold even when unchanged

Fix a valid finding from GitHub Copilot's review of HEAD b9d9053 (#2948):
register_enabled_presets_for_agent() normalizes a legacy flat-list
registered_skills value (predating per-agent provenance) to the
{agent_name: [...]} dict shape in memory via _normalize_registered_skills,
but the persistence check only compared the two *normalized* forms. When
the freshly rescaffolded skill names are identical to what the legacy
list already held — the common case, since nothing about the preset or
skill actually changed — that comparison is a no-op and registry.update()
is skipped, leaving the *raw* on-disk value as the un-migrated flat list.

A later switch to a different skill-mode agent and removal then follows
_unregister_skills's legacy best-effort path (restore only the currently
active agent's directory) instead of the per-agent provenance path,
permanently orphaning the first agent's override.

Fix: track the raw (pre-normalization) existing value and force
persistence whenever it's a non-empty list, independent of whether the
normalized content changed. Traced registered_commands for the same
class of bug: its registry value has always been Dict[str, List[str]]
(no legacy flat-list format ever existed for it — the existing
`if not isinstance(existing_commands, dict): existing_commands = {}`
guard is not a lossy migration path), so this fix stays scoped to
registered_skills only.

Added red-first regression test
test_rescaffold_migrates_legacy_flat_list_registered_skills: installs a
preset, overwrites its registry entry with a raw legacy flat list,
rescaffolds the *same* active agent with unchanged skill names, and
asserts the raw registry is migrated to per-agent dict form. Extends the
scenario with a switch to a second skill-mode agent and preset removal
to prove both agents' directories restore cleanly instead of orphaning
the first. Failed against the prior code (raw value stayed a list) and
passes after the fix.

Targeted (tests/test_presets.py, tests/test_extensions.py: 692 passed)
and full (3938 passed, 109 skipped) suites and ruff check on changed
files pass clean.

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

* fix: reconcile before fallible skills phase, infer legacy skill provenance, and unregister stale extension artifacts on toggle

Three findings from the Copilot review on HEAD b9d9053/3a1e749:

1. `register_enabled_presets_for_agent()` only recorded a preset's command
   names into `affected_cmd_names` (the set later passed to
   `_reconcile_composed_commands`/`_reconcile_skills`) in the loop that ran
   *after* `_register_skills()`, inside the same per-preset `try` block. If
   `_register_skills` raised, the `except` caught it and `continue`d before
   that loop ever ran — so a preset whose commands phase already wrote real
   content to disk never got reconciled against the full priority stack,
   leaving its raw content in place instead of a project override or
   higher-precedence preset's content. Fix: record the manifest's command
   names immediately after the commands phase succeeds and persists, before
   calling the independently fallible `_register_skills()`.

2. The legacy flat-list `registered_skills` migration (added for the
   previous review round) attributed every name in the list to whichever
   agent was currently being (re)activated. If the first operation after
   upgrading from a pre-#2948 registry was a direct switch to a *different*
   skill-mode agent (e.g. a legacy Claude override, then `integration use
   codex` with no intervening Claude rescaffold), the migrated dict only
   recorded `{"codex": [...]}`, permanently losing Claude's actual
   provenance and orphaning its override on later removal. Fix: added
   `_infer_legacy_skill_provenance()`, which probes every configured
   skill-mode agent's directory (via the same safe, symlink-validated
   helpers already used for restore/removal) for a `SKILL.md` whose
   frontmatter records this exact preset as the owner
   (`metadata.source == "preset:<pack_id>"`). A name found under more than
   one directory is attributed to every matching agent (the preset may have
   been active while the user switched between several skill-mode agents
   before provenance tracking existed); names that can't be matched to any
   directory still fall back to the previously-active best-effort
   behaviour. Directory grouping for shared-path aliases (e.g.
   agy/codex/zed all resolving to `.agents/skills`) intentionally does not
   call `.resolve()` on the path, since doing so diverges from
   `project_root`'s own resolution state on platforms where a path
   component is itself a symlink (e.g. macOS's `/var` -> `/private/var`)
   and made every subsequent containment check spuriously fail.

3. `register_enabled_extensions_for_agent()` has the same command/skill
   mutual-exclusion gap the preset path had (fixed in a previous round):
   toggling `ai_skills` for the *same active* agent left the opposite
   mode's artifact behind. Command -> skills left the extension's
   `.agent.md` file and its `registered_commands[agent]` entry in place
   once `skills_mode_active` made the commands phase a no-op. Skills ->
   command left the extension's `SKILL.md` file in place, since an empty
   `_register_extension_skills()` result (because this agent's skills
   directory no longer resolves once `ai_skills` is off) was treated as
   "nothing to register" rather than "this was rendered here before and is
   now stale". This diverges from the preset path in one respect:
   `registered_skills` for extensions has always been a flat list with no
   per-agent provenance (extension skills are only ever rendered for the
   active agent, never per-preset-per-agent tracked), so the fix resolves
   ownership by checking which of the extension's tracked skill names
   still exist as directories under this specific agent's directory before
   removing them — mirroring the same technique `unregister_agent_artifacts`
   already uses for full agent deactivation, but scoped narrowly to firing
   only when a toggle is actually detected (`skills_mode_active` /
   `command_mode_active`), so a same-mode re-run never disturbs
   already-correct artifacts or a user's manual customizations.

Regression tests (all confirmed red before their respective fix, green
after):
- tests/test_presets.py::TestPresetSkills::test_rescaffold_reconciles_override_even_when_skills_phase_fails
- tests/test_presets.py::TestPresetSkills::test_rescaffold_legacy_flat_list_direct_switch_preserves_original_agent
- tests/test_extension_skills.py::TestExtensionSkillRegistration::test_rescaffold_toggle_command_to_skills_removes_stale_extension_command_file
- tests/test_extension_skills.py::TestExtensionSkillRegistration::test_rescaffold_toggle_skills_to_command_removes_stale_extension_skill_file

Verification: tests/test_presets.py + tests/test_extensions.py +
tests/test_extension_skills.py (753 passed), tests/integrations/ (1768
passed, 1 skipped), full suite `pytest tests -q` (3942 passed, 109
skipped), `ruff check` on changed files clean.

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

* fix: broaden legacy skill provenance inference to command-backed agents

_infer_legacy_skill_provenance() only probed agents whose registrar
config statically declares extension == "/SKILL.md", excluding
command-backed agents (e.g. Copilot) that can also render preset
overrides as SKILL.md files when ai_skills is enabled. A real
preset-owned .github/skills/.../SKILL.md written while Copilot was the
active skills-mode agent was therefore never probed and got
misattributed entirely to whichever agent activated first after the
upgrade, permanently orphaning Copilot's override on later removal.

Broaden the candidate set to every configured integration
(CommandRegistrar.AGENT_CONFIGS), reusing the existing safe-path
helper (_safe_skills_dir_for_agent, itself built on the shared
_get_skills_dir resolver) rather than inventing new path-construction
logic. The existing preset-marker match (metadata.source ==
"preset:<pack_id>") continues to gate every attribution, so
command-mode agents that never rendered this preset's skill are not
falsely attributed.

Add red-first regression tests: a legacy flat-list entry owned by
Copilot in skills mode, switched directly to Claude with no
intervening Copilot rescaffold, now migrates to a per-agent dict
covering both agents, and removal restores both agents' files instead
of orphaning Copilot's override; plus a negative-case test confirming
a command-mode Copilot with no preset-owned skill marker is not
falsely attributed during the same migration.

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

* fix: preserve extension skill tracking for mirrors in other agent dirs

The skills -> command toggle cleanup in
register_enabled_extensions_for_agent() recomputed the remaining
tracked registered_skills names by checking only the toggling agent's
own skills directory. Since registered_skills is a single flat list
shared across every agent an extension has ever been activated under
(skills are only ever rendered for the active agent, so there is no
per-agent registry key), a name whose mirror still existed under a
*different*, previously-active agent's directory was incorrectly
dropped from tracking as soon as the current agent's own copy was
removed. A later full removal only iterates registered_skills, so the
orphaned mirror under the other agent's directory was never found or
cleaned up.

Add _extension_owned_skill_names(), which re-verifies ownership across
every configured agent's skills directory (deduped by shared path) the
same way the existing _unregister_extension_skills() fallback scan
already does, keeping a name only when a SKILL.md with a matching
metadata.source == "extension:<id>" marker is found somewhere -
read-only, no directory creation, no symlink escape. Use it instead of
re-checking only the toggling agent's own directory when recomputing
what remains tracked after narrow stale-mirror cleanup.

Add a red-first regression test: Auggie is activated in skills mode
first (writing a mirror), then Copilot is activated in skills mode
(writing its own mirror for the same names), then Copilot toggles to
command mode. Before the fix, registered_skills lost both names
entirely even though Auggie's mirrors were untouched on disk; after
the fix tracking is preserved and a subsequent full removal correctly
cleans up Auggie's remaining mirrors too.

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

* fix: reject symlinked skills-directory escape in extension skill scans

_extension_owned_skill_names() and the fast/fallback paths of its
sibling _unregister_extension_skills() called skills_candidate.resolve()
and then checked children relative to that already-resolved candidate.
If the candidate directory itself (e.g. .gemini/skills) was a symlink
pointing outside the project root, both the resolve() call and the
subsequent containment check silently passed through the symlink
instead of rejecting it:

- _extension_owned_skill_names() would falsely attribute ownership to
  a marker-matching SKILL.md living outside the project.
- _unregister_extension_skills()'s fast path (an explicit skills_dir,
  as passed by the toggle-cleanup call site) and its fallback scan
  (used during full extension removal) would both shutil.rmtree() the
  external directory, deleting unrelated content outside the project.

Fix by validating the candidate directory itself with the existing
_validate_safe_shared_directory() shared-infra helper before any probe
or delete: it rejects a symlink at any path component (walking down
from the project root, including the final component) without ever
resolving through it, and is already used elsewhere in the codebase for
the same class of shared-directory containment check. Unsafe
candidates are skipped/refused rather than followed.

Add red-first security regression tests reproducing each of the three
call sites with a `.gemini/skills` symlink pointing at an external
directory containing a marker-matching SKILL.md and an unrelated
precious_file.txt: provenance inference must not attribute the name,
and both the explicit-skills_dir fast path and the None-skills_dir
fallback scan must leave the external directory and file untouched.
Existing valid shared/deduped directory tests (e.g. agy/amp/codex/zed
sharing .agents/skills) continue to pass, confirming legitimate shared
directories still clean up correctly.

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

* Fix unscoped extension-skill removal and legacy preset provenance on direct remove

- _unregister_extension_skills(): omitting skills_dir now always triggers
  the full multi-directory fallback scan instead of narrowing to the
  currently active agent's directory. Previously, remove() (the only
  caller that omits skills_dir) would resolve the active agent's dir and
  take the scoped fast path, orphaning a previously-active second agent's
  extension skill mirror during full removal.

- PresetManager.remove(): infer legacy flat-list registered_skills
  provenance (reusing _infer_legacy_skill_provenance from the prior
  rescaffold fix) before invoking _unregister_skills, so a direct
  `preset remove` with no intervening rescaffold/switch also restores
  every previously-active agent's directory instead of only the
  currently active one.

Added regression tests:
- test_remove_while_second_agent_still_in_skills_mode_cleans_up_first_agent_mirror
- test_remove_infers_legacy_flat_list_provenance_without_prior_rescaffold

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

* Keep unregister_agent_artifacts scoped to its agent when directory is absent

ExtensionManager.unregister_agent_artifacts() converted its resolved
agent_skills_dir to None whenever that directory didn't exist, before
calling _unregister_extension_skills(). After 1d8f9e3, omitting
skills_dir means "genuinely unscoped removal": scan every configured
agent's directory, reserved for ExtensionManager.remove()'s full
project cleanup. Since unregister_agent_artifacts is agent-scoped (used
by switch to clean up the previous integration's artifacts), this
caused it to delete every other agent's live extension skill mirrors
whenever the target agent's own directory happened to be absent, e.g.
unregistering an agent that was never activated.

Fix: always pass the explicit, agent-scoped skills_dir, even when it
doesn't exist on disk, so the fast path is a safe no-op for an absent
directory instead of falling back to the all-agents scan. Registry
reconciliation (dropping removed names from the flat registered_skills
list) now only runs when the agent's directory actually exists, so an
absent directory can't be misread as "these names were removed
everywhere" and wipe tracking for mirrors that still legitimately live
under other agents' directories.

Added regression test:
- test_unregister_agent_artifacts_stays_scoped_when_agent_dir_absent

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

* Preserve global skill tracking across agents in unregister_agent_artifacts

The present-directory branch of ExtensionManager.unregister_agent_artifacts()
recomputed "remaining" registered_skills only by checking whether each name
still existed under the just-cleaned agent's own directory. registered_skills
is a single flat list shared across every agent an extension was ever
activated under (skills are only ever rendered for the currently active
agent, so there's no per-agent registry key). Repro: auggie and copilot both
have mirrors for the same extension; unregister_agent_artifacts("auggie")
correctly removes auggie's own mirror, sees the names absent from auggie's
(now empty) directory, and stores an empty registered_skills list - even
though copilot's mirror is still live on disk and now untracked. A later full
remove() then reads an empty registry and leaves copilot's mirror orphaned.

Fix: after the agent-scoped cleanup, recompute remaining names with
_extension_owned_skill_names(), which scans every safe, configured agent
skills directory (not just the one just cleaned) and keeps a name only if a
marker-verified SKILL.md for this extension still exists somewhere. This is
the same helper already used for the analogous same-agent toggle-cleanup
case, so no new abstraction was introduced. Explicit per-agent cleanup,
marker ownership verification, and symlink/containment safety are unchanged.

Added regression test:
- test_unregister_agent_artifacts_preserves_tracking_for_other_agent_mirror

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

* Reconcile every historical agent on preset removal; validate child skill dirs

Fixes 3 findings from the Copilot review on HEAD 31c9b97 (#2948):

1. presets/__init__.py: remove()'s command reconciliation only recreated
   the surviving preset's content for the currently active agent, even
   though the removed preset's registered_commands could span multiple
   historical (now-inactive) agents recorded via prior rescaffolds. Now
   remove() captures every historical agent registered_commands actually
   targeted (before mutation) and passes it as extra_agents through
   _reconcile_composed_commands -> _register_for_non_skill_agents /
   _register_command_from_path -> registrar.register_commands_for_non_
   skill_agents, so the active-only restriction for install/use is
   preserved while post-removal reconciliation restores every touched
   directory.

2. presets/__init__.py: the analogous gap existed for skills. _unregister_
   skills() now returns {skills_dir: renderer_agent} for every directory it
   actually restored, and _reconcile_skills() accepts extra_skills_dirs to
   reconcile each of those directories (via a new apply_to_dir() helper),
   not only the currently active skills directory. _register_skills() gained
   optional target_dir/target_agent overrides (forcing
   create_missing_skills off for non-active directories) so a historical
   directory is only ever restored, never seeded with brand-new skills.

3. extensions/__init__.py: _extension_owned_skill_names() and both the
   fast and fallback paths of _unregister_extension_skills() validated only
   the parent skills_dir for symlink escape, then resolved
   skills_dir / skill_name and checked containment relative to that
   already-resolved parent. A per-skill child that is itself a symlink to
   a different, legitimate skill directory within the same (safe) root
   passed that containment check, so deleting/attributing through the
   symlink name could destroy or misattribute an unrelated skill reached
   only via the alias. All three call sites now run the shared
   _validate_safe_shared_directory() component-wise check against the full
   skills_dir / skill_name path (not just the parent) before any read or
   delete, rejecting a symlinked child outright rather than following it,
   even when its resolved target remains in-bounds.

Regression tests added (all confirmed red against pre-fix code, green
after):
- test_remove_reconciles_command_for_every_historical_agent
- test_remove_reconciles_skill_for_every_historical_agent
- test_extension_owned_skill_names_rejects_symlinked_child_skill_dir
- test_unregister_extension_skills_explicit_dir_rejects_symlinked_child
- test_unregister_extension_skills_fallback_rejects_symlinked_child

Tests: tests/test_presets.py (361), tests/test_extension_skills.py (69),
tests/test_extensions.py (338) all pass; tests/integrations (1768 passed,
1 skipped) pass; full suite 3902 passed / 74 skipped (90 pre-existing,
environment-only git-signing tests deselected — confirmed failing
identically on the pre-change baseline due to local 1Password SSH-agent
signing, unrelated to this change). ruff check clean on all changed files.

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

* Persist historical reconciliation ownership; defer destructive toggle cleanup; validate registry-provided skill names

Round 11 review findings (5 comments on HEAD ab6c28c), three root causes:

A) Historical-agent reconciliation wrote surviving content to disk but
   discarded the returned per-agent write map, so the preset's own
   registered_commands/registered_skills never learned about directories
   reconciliation restored on its behalf. A later removal of that same
   preset then orphaned those directories. Added
   _merge_pack_registered_commands/_merge_pack_registered_skills and wired
   them into _reconcile_composed_commands and _reconcile_skills's
   apply_to_dir so every actual write is merged back into the winning
   preset's registry metadata.

B) Command<->skills toggle on an already-active agent deleted the old
   artifact before the replacement registration ran, in both
   presets/__init__.py's register_enabled_presets_for_agent and
   extensions/__init__.py's register_enabled_extensions_for_agent. If the
   replacement step raised, both artifacts were lost. Deferred the
   destructive cleanup until after the replacement phase completes
   without raising (register-new-then-remove-old ordering); the mirror
   skills->command direction was already safe since the new command file
   is always registered unconditionally before any cleanup runs.

C) _unregister_skills_in_dir and _infer_legacy_skill_provenance joined a
   registry-provided (untrusted) skill name directly onto a directory
   before any name-shape validation. An absolute in-project name discards
   the intended parent directory entirely (Path's "/" operator drops the
   left side for an absolute right side), letting a corrupted registry
   entry escape the intended skills subtree while still resolving inside
   the project root - passing the existing containment/symlink check.
   Added a centralized _is_safe_registry_skill_name guard (rejecting
   non-strings, empty strings, absolute paths, multi-component paths, and
   "."/".." ) and applied it before every path join derived from
   registry-provided skill names in both functions. Also fixed
   _infer_legacy_skill_provenance's unmatched-name fallback, which
   previously still attributed rejected names to fallback_agent even
   after the loop skipped them.

Added red-first regressions for all three root causes, covering: a
two-preset historical-command-agent survivor scenario, an analogous
skill-agent survivor scenario, injected skills-phase failure during a
preset command->skills toggle and the extension equivalent, a direct
unit test of the new name-safety guard, an absolute-path escape attempt
against _unregister_skills_in_dir, and a false-attribution attempt
against _infer_legacy_skill_provenance.

Tests: tests/test_presets.py (367 passed), tests/test_extension_skills.py
+ tests/test_extensions.py (408 passed), tests/integrations (1768
passed, 1 skipped), full suite tests -q deselecting the pre-existing
1Password-signing-affected tests/extensions/git/test_git_extension.py
(3909 passed, 74 skipped, 90 deselected). ruff check clean on all
changed files.

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

* Verify replacement actually landed before retiring stale toggle artifacts

The command<->skills toggle cleanup added for #2948 deferred destructive
removal of the old-mode artifact until after the replacement registration
call completed without raising. That was necessary but not sufficient:
none of _register_skills(), _register_commands(),
register_commands_for_agent(), or _register_extension_skills() raise on
a missing source template, a safety-validation skip, or a corrupted
manifest entry — they simply return an empty or partial result. Treating
"did not raise" as "fully replaced" meant a stale artifact could still be
deleted (or its tracking dropped) even though its specific replacement
never actually landed, leaving neither artifact in place for that logical
command/skill.

Fix all four affected toggle directions by checking the replacement
call's actual return value before allowing any destructive step:

- presets command->skills (register_enabled_presets_for_agent): only
  unregister a stale command name once its corresponding skill name
  (via the existing _skill_names_for_command() helper) is confirmed
  present in the skills call's returned names for that agent; the
  remainder stays tracked and on disk.
- presets skills->command (register_enabled_presets_for_agent): only
  unregister a stale skill name once its corresponding command name is
  confirmed present in the commands call's returned names for that
  agent, using the same helper.
- extensions skills->command (register_enabled_extensions_for_agent):
  only remove a skill mirror once the matching command (mapped via the
  existing HookExecutor._skill_name_from_command() helper) is confirmed
  present in register_commands_for_agent's returned names.
- extensions command->skills (register_enabled_extensions_for_agent):
  only remove a deferred stale command once its matching skill name is
  confirmed present in _register_extension_skills()'s returned names.

All four reuse the existing command<->skill name-derivation helpers
rather than inventing new mapping logic. Registry tracking is updated to
retain exactly the unreplaced subset rather than being popped wholesale,
so partially-successful toggles leave correct, minimal tracking behind.

Added 8 new regression tests (4 presets, 4 extensions) covering both the
fully-empty and genuinely-partial result cases for each of the four
toggle directions, using real missing-source-file scenarios (not mocked
return values) to exercise the actual code paths. Confirmed red before
the fix and green after for all 8.

Focused (test_presets.py, test_extension_skills.py, test_extensions.py,
tests/integrations): 2551 passed, 1 skipped.
Full suite (tests, excluding the pre-existing environment-local
1Password-signing git-extension failures): 3917 passed, 74 skipped, 90
deselected.
ruff check: clean.

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

* Retire alias command groups on toggle; scope preset cleanup to switched-away agent (#2948)

Fixes three current Copilot review findings on HEAD d0d152e:

1. Command->skills toggle cleanup only matched a stale command's own
   name against the returned replacement skill name. Aliases
   (CommandRegistrar tracks and returns primary + alias names flattened
   into one list) never have their own skill rendered -- only the
   primary command's skill is rendered -- so an alias's name could never
   match, leaving its command artifact and tracking behind forever even
   after the primary's replacement landed. Fixed identically in both
   presets (register_enabled_presets_for_agent) and extensions
   (register_enabled_extensions_for_agent): build a primary->alias
   mapping from the manifest, group stale names by primary, and
   retire/keep the whole group together based solely on whether the
   primary's skill replacement actually landed.

2. `integration switch` to a not-yet-installed target unregistered the
   old agent's extension artifacts but had no preset equivalent, so a
   preset's command overrides (including custom preset commands) and
   skill mirrors for the deactivated agent lingered as orphans. Added
   `PresetManager.unregister_agent_artifacts()`, mirroring
   `ExtensionManager.unregister_agent_artifacts()`: scoped strictly to
   the given agent, migrates a legacy flat-list `registered_skills`
   entry via existing on-disk provenance inference before removing
   anything (so other agents' real ownership is preserved rather than
   guessed or dropped), and guards against double-processing an
   artifact through both the commands and skills paths for native
   SKILL.md agents. Wired via a new `_unregister_presets_for_agent()`
   helper into the integration switch command's existing old-agent
   cleanup phase.

Added red-first regression tests:
- tests/test_presets.py: alias-group retire/keep/partial-multi-group
  tests for the command->skills toggle; unregister_agent_artifacts
  scoping tests for commands and legacy-list skill provenance.
- tests/test_extension_skills.py: alias-group retire/keep tests for the
  extension command->skills toggle.
- tests/integrations/test_integration_subcommand.py: end-to-end switch
  test proving a preset's custom command override is cleaned up when
  switching to a not-yet-installed integration, with tracking updated
  correctly and the new agent's registration unaffected.

All new tests confirmed red (AttributeError / orphaned file assertions)
before the fix and green after. Full suite: 3980 passed, 109 skipped.
ruff check clean on all changed files.

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

* fix: track reconciled extension artifacts

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

* Fix native skill preset reconciliation

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

* Fix shared native skill cleanup

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

* Fix partial preset rescaffold tracking

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

* Fix preset agent skill lifecycle

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

* Clarify preset removal reconciliation

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

* fix(integrations): address upgrade review feedback

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

* fix(presets): reconcile partial command writes

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

* fix: address active artifact cleanup review

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

* fix: defer preset skill cleanup to winning command

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

* fix: track reconciled and partial preset skills

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

* fix: reconcile project overrides to legacy skills

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

* fix: harden preset skill writes and rollback

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

* fix(presets): harden legacy skill restoration

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

* fix(presets): preserve non-owned legacy skills

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

* fix: validate reconciled skill paths

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

* fix(presets): preserve reconciled skill ownership

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

* fix(presets): clean reconciled agent skills

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

* fix: keep legacy cleanup project-local

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

* fix(presets): keep active agent's artifacts in its current mode on remove

A partially failed command<->skills toggle leaves stale tracking
(registered_commands or registered_skills) for the active agent, and
remove() replayed that history regardless of the agent's current mode:

- extra_agents re-admitted the active skills-mode agent into command
  reconciliation, recreating its command file from a surviving lower
  preset even though only_agent excluded it.
- _unregister_skills restored (and _reconcile_skills reapplied) a skill
  artifact for the active command-mode agent instead of deleting the
  preset-owned leftover.

The active agent's participation is now decided exclusively by its
current mode: reconciliation strips it from extra_agents, and removal
routes its stale skills through _delete_agent_preset_skills. Historical
replay still applies to inactive agents only (#2948).

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

* fix: filter uninstalled-extension commands in reconciliation; allow active-agent layout change with presets

Two follow-ups to the upstream-main merge:

- Preset reconciliation (_reconcile_composed_commands) now skips
  extension-scoped commands (speckit.<ext>.<cmd>) whose extension is not
  installed, at the single chokepoint every install/remove/rescaffold
  pass funnels through. Registration already refused them, so
  reconciliation could materialize files no registry entry tracks. The
  duplicated per-call-site filters collapse into one
  _extension_installed_for_command helper.

- The #3415 layout-change guard predates this PR's agent-scoped preset
  rescaffold: for the active integration, _register_presets_for_agent
  now re-registers enabled presets in the new layout and retires the
  old layout's stale files, so an active-agent command<->skills toggle
  proceeds and reconciles instead of being rejected. The guard still
  rejects non-active agents (no rescaffold runs for them) and still
  fails closed on an unreadable registry.
  _installed_presets_affecting_agent also understands the per-agent
  dict shape of registered_skills this PR writes, instead of raising
  'malformed'.

Regression tests: rescaffold with an uninstalled extension's command,
CLI-level legacy<->skills toggle with an installed preset (both
directions), secondary-agent rejection, and dict-shaped
registered_skills in the guard helper.

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

* fix: reject active layout change while a disabled preset owns artifacts

The post-upgrade preset rescaffold iterates enabled presets only, and a
disabled preset's artifacts are deliberately frozen until removal, so an
active-agent command<->skills layout change cannot reconcile them.
_installed_presets_affecting_agent now reports each preset's enabled
state and the guard rejects the migration while any affected preset is
disabled, with re-enable/remove guidance. Enabled presets and non-active
rejection behave as before.

Regression test: disabled preset blocks the toggle untouched; re-enabling
unblocks it and reconciles.

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

* docs: replace placeholder prefix in two safety comments

Comment-only: spell out why skill deletion is restricted to
project-local directories (flat/legacy provenance cannot prove
home-directory ownership) instead of an undefined placeholder word.

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

* docs: correct guard-helper docstring to active-only registration model

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

* fix: fail closed on non-list values in per-agent preset provenance

A dict-shaped registered_skills/registered_commands entry with a
non-list value (e.g. null) left ownership undecidable but read as "no
artifacts", letting a layout-changing upgrade proceed on a malformed
registry. Validate values are lists and raise
_PresetRegistryUnreadableError otherwise, matching the guard's
fail-closed contract. Unit test covers both fields.

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

* fix: drop eager extension unregister on layout-changing upgrade

Unregistering the agent's extension artifacts before re-registration
deleted files and registry tracking up front, so a failed or partial
re-registration left the extension with no artifacts at all. Retirement
of each opposite-mode artifact already belongs to
register_enabled_extensions_for_agent's deferred toggle cleanup, which
removes an old artifact only after its replacement is confirmed. Also
keeps disabled extensions consistent with disabled presets: artifacts
stay frozen in place with intact tracking.

Regression test corrupts the installed extension manifest so
re-registration fails, then asserts the old-layout artifacts and their
registry tracking survive the upgrade.

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

* fix: rescaffold fallback integration after failed switch rollback

When Phase 2 of a switch fails, rollback restores another installed
integration as the default via _set_default_integration but never
re-registered extensions or presets for it. Under active-only
registration the fallback may never have received any artifacts (it
was installed while another integration was active), and Phase 1
already unregistered the outgoing agent's artifacts — leaving the
restored default unusable. Rescaffold both extensions and presets
(best-effort) after the fallback default is successfully restored.

Regression test: secondary codex install with the git extension, a
failing switch to generic, then asserts codex ends up with registered
extension artifacts after rollback.

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

* docs: explain load-bearing pre-create loop in _reconcile_skills

The per-skill _validate_skill_subdir(create=True) loop looks like dead
code (its result is unused), but it re-creates the tracked skill
subdirectories that _unregister_skills just deleted so
_register_skills's only-overwrite-existing gate passes during a
historical-directory restore. Removing it fails
test_skill_reconciliation_preserves_per_directory_names.

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

* fix: preserve dashed-description skill tracking

Use the shared frontmatter parser when verifying surviving extension skill mirrors so delimiter substrings cannot hide provenance metadata.

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

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

* fix: skip absent extension skills during reconciliation

Filter extension-scoped commands before skill reconciliation so historical preset tracking and project overrides cannot recreate artifacts for uninstalled extensions.

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

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

* fix: preserve partial native skill cleanup

Coordinate native-skill command cleanup with registered skill coverage per agent and command so partial rescaffolds cannot orphan preset artifacts.

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>
2026-07-27 14:09:40 -05:00
Noor ul ain
42c7230aa9 fix(extensions): tolerate non-string tags in catalog search (#3746)
* fix(extensions): tolerate non-string tags in catalog search

ExtensionCatalog.search() assumed catalog `tags` were always strings:
the tag filter called `t.lower()` and the query path did
`" ".join([...] + tags)`. Extension catalog JSON is user-editable, so a
hand-authored `tags: [1, 2]` crashed search with AttributeError (tag
filter) or TypeError (query join).

Coerce defensively by filtering to `isinstance(t, str)` and guarding the
tags value as a list, matching the reference-correct sibling in
integrations/catalog.py. Non-string tags are now skipped rather than
raising.

Adds a regression test driving search(tag=...) and search(query=...)
against a catalog with mixed string/int tags; both fail pre-fix.

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

* fix(extensions): also coerce non-string author/name in catalog search

The same ExtensionCatalog.search() method had two more string
assumptions on user-editable catalog fields: the author filter called
`ext_data.get("author", "").lower()` (AttributeError on a numeric
author) and the query searchable-text joined `name`/`description`
uncoerced (TypeError on a numeric name). Coerce both defensively,
matching the reference-correct integrations/catalog.py::search.

Extends the regression test with non-string author/name coverage;
fails pre-fix with AttributeError at the author filter.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 11:58:23 -05:00
Quratulain-bilal
e6a3ccfb27 fix(extensions): hyphenate command names in 'extension info' listing (#3744)
The 'Commands:' section of 'specify extension info' for a locally installed
extension printed each command in its manifest dotted form
(e.g. speckit.jira.sync). Cline and Forge register hyphenated command names
(/speckit-jira-sync), so on those projects the displayed names did not match
what the user actually invokes.

Format each name through the active agent's command-name formatter, mirroring
the parity 'extension add' already applies to its 'Provided commands' listing
(#3669) and completing the Forge/Cline command-name parity from #3641/#3642.

Adds a regression test asserting the hyphenated form appears (and the dotted
form does not) for a Forge project.
2026-07-27 11:56:21 -05:00
Noor ul ain
962f9f0765 fix(workflows): escape remaining untrusted fields in workflow info (#3731)
* fix(workflows): escape remaining untrusted fields in `workflow info`

Follow-up to #3690, which escaped only the step-graph brackets. Every
other metadata field `workflow info` prints is untrusted content
(workflow.yml or catalog JSON), and console.print has Rich markup
enabled, so an unescaped `[...]` in any of them is parsed as a style tag
and silently swallowed:

- definition path: name, version, author, description, integration, and
  each input's name/type
- catalog path: name, version, description, tags, and the "not found"
  workflow id

A description of `Does [stuff] nicely` rendered as `Does  nicely`; an
integration of `claude [code]` rendered as `claude `. Route every field
through _escape_markup, matching the sibling `workflow list` / catalog
`search` commands, so bracketed text renders literally.

Add two regression tests covering the definition and catalog paths; both
fail on the pre-fix source (fields with brackets come back truncated).

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

* test: cover version + not-found-id escapes in workflow info

Addresses Copilot review feedback on the workflow-info markup-escape
tests:

- The definition-path and catalog-path regression tests left `version`
  bracket-free and never asserted it, so the version escapes could be
  removed without failing. Use bracketed version values and assert they
  survive verbatim.
- The newly escaped not-found identifier is a separate output path that
  no test reached. Add a case where local load raises FileNotFoundError
  and catalog lookup returns None, invoke `workflow info` with a
  bracketed ID, and assert the literal ID is preserved in the error.

Verified each new assertion fails when its source escape is removed
(test-the-test).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 11:55:00 -05:00
Ali jawwad
c1028e5506 fix(extensions): guard non-numeric catalog downloads in search/info rendering (#3710)
* fix(extensions): guard non-numeric catalog downloads in search/info rendering

`specify extension search` and `specify extension info <id>` format a catalog
entry's `downloads` field with the `:,` thousands separator, guarded only by
`is not None`. Catalog payloads are only shape-validated -- individual fields
are never type-checked and `_get_merged_extensions` returns raw catalog dicts
-- so an entry with a non-numeric `downloads` (e.g. the JSON string "1500",
realistic from a community / SPECKIT_CATALOG_URL / project catalog) makes the
`:,` format raise `ValueError: Cannot specify ',' with 's'`, aborting the
whole command with an uncaught traceback.

Group-format `downloads` only when it is actually numeric; otherwise render it
as-is. Numeric values (int/float, incl. bool) format identically, so correct
catalogs are byte-for-byte unchanged. Every other field in these two renderers
is already `str()`-wrapped; this closes the one unguarded field.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* fix(extensions): escape the non-numeric downloads fallback for Rich markup

Address review feedback: the fallback interpolated the untrusted catalog value
straight into a Rich-rendered string, so guarding the ``:,`` ValueError just
traded it for a MarkupError -- a catalog entry with downloads "[/red]foo" still
aborted `extension search`/`info`, and balanced tags could restyle the output.

Wrap the fallback in _escape_markup(str(...)) at both sites, matching how every
other catalog field in these renderers is already escaped. Numeric values keep
the identical ``:,`` branch, so correct catalogs are unchanged.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* fix(extensions): escape 'stars' too, in the same stats string

Follow-up to the downloads escaping: `stars` is the other catalog-controlled
value joined into the same Rich-rendered stats line, and it was still raw --
verified that stars "[/red]x" raises the same MarkupError and aborts
`extension info`/`search`. Hardening one of the two adjacent values would have
left the reported defect reachable through the sibling field.

🤖 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>
2026-07-27 11:25:01 -05:00
Mateus Cardoso
9e150cd3b2 fix(agent-context): apply default markers when config markers are blank (bash) (#3736)
When the extension config omits context_markers (or sets them blank),
relying on the built-in defaults, the Bash port aborted with "malformed
config parser output" and never updated the context file, while the
Python (`or DEFAULT_*`) and PowerShell (default-initialized) ports handled
it correctly.

The config parser prints three lines (context_files JSON, marker_start,
marker_end), captured via `_raw_opts="$(...)"`. Command substitution strips
trailing newlines, so blank marker lines collapse the output to fewer than
three, tripping the `(( ${#_opts_lines[@]} < 3 ))` guard and making the
DEFAULT_START/END substitution unreachable — the exact case it was written
for.

Require only the context_files line and default the marker lines to empty
(`${_opts_lines[1]:-}` / `${_opts_lines[2]:-}`) so the existing
DEFAULT_START/END fallback fills them in. Add a parity regression test with
blank markers (it fails on the old guard and passes with the fix).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:33:56 -05:00
Noor ul ain
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>
2026-07-27 10:15:37 -05:00
Ali jawwad
103ad73775 fix(workflows): guard non-mapping 'workflow:' block in WorkflowDefinition (#3694)
* fix(workflows): guard non-mapping 'workflow:' block in WorkflowDefinition

A present-but-non-mapping top-level `workflow:` block (bare `workflow:` ->
YAML null, or `workflow: <str>` / `workflow: [..]`) crashed
WorkflowDefinition.__init__ with AttributeError: the `{}` default of
`data.get("workflow", {})` only applies when the key is ABSENT, so a non-dict
value reached `workflow.get("id", ...)`. This fires inside from_yaml/
from_string — before validate_workflow can report the malformed shape — and
in the CLI escapes as a raw traceback (load_workflow is wrapped to catch only
FileNotFoundError/ValueError).

Normalize the local `workflow` to {} when it is not a mapping (self.data keeps
the raw value so validate_workflow still reports it), mirroring the adjacent
default_options guard.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* test(workflows): assert self.data preserves the raw non-mapping workflow value

Address review: the previous assertion only proved the key stayed present; it
would pass even if construction replaced the malformed value with {}. Assert
definition.data["workflow"] equals the original parsed value and is still a
non-mapping, proving the guard normalizes only the local variable.

🤖 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>
2026-07-27 09:10:51 -05:00
Ali jawwad
59e63699b8 fix(bundler): reject unsupported schema_version in _merge_config (align readers) (#3711)
bundle-catalogs.yml has two readers that are meant to agree: commands_impl/
catalog_config._read (bundle catalog list/add/remove) and models/catalog.
_merge_config (the resolution path feeding bundle search/info/install via
load_source_stack). _read rejects an unsupported MAJOR schema_version;
_merge_config never checked it, so a file written by a newer/incompatible
Spec Kit (e.g. schema_version '2.0') was silently parsed under v1 assumptions
on the exact path where install_policy governs trust — the two readers
disagreed. #3623 (non-list catalogs) and #3659 (top-level non-mapping) already
aligned these two readers guard-by-guard; this is the last unaligned guard.

Add the same forward-compatible major-version check to _merge_config. Promote
CONFIG_SCHEMA_VERSION to models/catalog.py as the single source of truth and
import it in catalog_config.py (was a local duplicate) so the two cannot drift.
Absent schema_version stays valid (backward compatible); matching major stays
valid.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 08:19:01 -05:00
github-actions[bot]
015d125667 Update Linear Weave extension to v1.0.1 (#3762)
Update linear-weave extension submitted by @tonydwoodhouse:
- extensions/catalog.community.json (version, download_url, documentation, updated_at)

Closes #3758

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-27 08:17:41 -05:00
github-actions[bot]
3ca0eb169e Add Intake Sequencing Governance preset to community catalog (#3761)
Add intake-sequencing-governance preset submitted by @hindermath to:
- presets/catalog.community.json (alphabetical order)
- docs/community/presets.md community presets table

Closes #3742

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-27 08:16:57 -05:00
github-actions[bot]
c6cb25cb4a Update Quality Gates (Enforcement Layer) extension to v0.3.3 (#3760)
Update gates extension submitted by @schwichtgit:
- extensions/catalog.community.json (version, download_url, updated_at)
- docs/community/extensions.md community extensions table (no changes needed)

Closes #3755

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-27 08:15:33 -05:00
github-actions[bot]
eb8108e7b3 Update Verify Review Ship extension to v0.4.1 (#3759)
Update verify-review-ship extension submitted by @cadugevaerd to:
- extensions/catalog.community.json (version, download_url, sha256, description, requires, provides, tags, updated_at)
- docs/community/extensions.md community extensions table

Closes #3751

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-27 08:00:08 -05:00
Mateus Cardoso
403fcdc6fd fix(agent-context): discover nested plans in Python port mtime fallback (#3734)
The Python port of update-agent-context reintroduced a one-level plan
scan (specs/*/plan.md) in its mtime fallback, while the Bash and
PowerShell ports search recursively (specs/**/plan.md) per the fix for
issue #3024. The three ports were therefore not in parity: for nested
scoped layouts such as specs/<scope>/<feature>/plan.md, the Python port
found no plan and omitted the plan link from the managed context section.

Switch the fallback to `(root / "specs").rglob("plan.md")` and update the
module docstring to match the documented recursive-discovery contract.
Add a parity regression test covering the nested layout (it fails on the
one-level glob and passes with the recursive scan).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 07:41:20 -05:00
Oscar
2fb94e0f9c fix(extensions): make shipped scripts executable after install (#3723)
Extension archives are unpacked with zipfile.extractall and directory installs
are copied; neither restores a stripped Unix mode. A bundled *.sh therefore
lands non-executable, so a documented `.specify/extensions/<id>/scripts/bash/foo.sh`
invocation fails with "Permission denied" — e.g. a CI step that runs an
extension's gate. It only worked incidentally, after a later `specify init`.

Restore permissions at the shared sink. Every extension install route funnels
through ExtensionManager.install_from_directory (install_from_zip delegates to
it; extension add, extension update, and bundle installs all reach it), so
calling the existing ensure_executable_scripts() there covers every route —
present and future — by construction rather than by patching each command.

The helper already makes .specify scripts executable (init, migrate, and
integration-install all call it); it is called plainly, re-establishing the same
idempotent "scripts are executable" invariant those flows restore. Deliberately
the whole-project call rather than a scoped one: a scan-scope argument would only
spare re-walking already-correct files — negligible beside the copy/extract just
performed — while widening a simple, widely-used interface for a single caller.
Existing callers were audited: init's end-of-init call still covers core
.specify/scripts and is untouched; integration-install and migrate do no manager
install. Nothing is removed. No-op on Windows; best-effort per file; does not
change which files are executable or their mode.

Tests: a manager-level regression test asserts a mode-0644 script comes out
executable via both install_from_directory and install_from_zip(force=True) (the
latter also covering the remove-then-reinstall shape of extension update), plus
an end-to-end `extension add --dev` test. Both fail without the change; skipped
on Windows.

Fixes #3722.
2026-07-27 07:40:43 -05:00
Manfred Riem
446ee329b1 docs(assess): clarify the pipeline works on an empty project (#3732)
* docs(assess): clarify the pipeline works on an empty project

State explicitly in the README and intake command that the assess
pipeline requires no existing source code. An empty, freshly
initialized project and an existing codebase are equally valid
starting points — the input is just an idea (pasted text, a URL, a
ticket, or a codebase pointer).

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

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9df2615e-6a99-4cdc-b4b2-fc72029bc1d9

* docs(assess): distinguish empty project from no project

Clarify that assess still runs inside an initialized Spec Kit project
(writing under .specify/assessments/) — only existing source code is
optional. Reword 'no repo at all'/'need no repo' to 'need no existing
codebase' so users don't expect intake to work outside a Spec Kit
project.

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

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9df2615e-6a99-4cdc-b4b2-fc72029bc1d9

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9df2615e-6a99-4cdc-b4b2-fc72029bc1d9
2026-07-27 06:42:35 -05:00
Manfred Riem
c0fe0e43cd chore: release 0.14.2, begin 0.14.3.dev0 development (#3730)
* chore: bump version to 0.14.2

* chore: begin 0.14.3.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-24 15:42:56 -05:00
github-actions[bot]
ae0b8ca2b0 Update Intake Review Governance preset to v0.1.1 (#3729)
Update intake-review-governance preset submitted by @hindermath to:
- presets/catalog.community.json (version, download_url, documentation, description)
- docs/community/presets.md community presets table

Closes #3727


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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-24 15:33:14 -05:00
github-actions[bot]
2fe35bf898 Update Verify Review Ship extension to v0.3.0 (#3728)
Update verify-review-ship extension submitted by @cadugevaerd:
- extensions/catalog.community.json (version, download_url, description, effect, tags, sha256, updated_at)
- docs/community/extensions.md community extensions table

Closes #3726

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-24 15:28:25 -05:00
Dhruv Rastogi
73908f798a Update Architecture Guard extension to v1.13.1 (#3724)
Update architecture-guard extension submitted by @DyanGalih:
- extensions/catalog.community.json (version 1.8.17 -> 1.13.1, download_url,
  provides.commands 10 -> 14, tags: add hygiene, updated_at)

Closes #3564

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

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-24 13:10:55 -05:00
Ali jawwad
ffe2a7ffd6 docs(upgrade): Claude Code files live in .claude/skills, not .claude/commands (#3708)
The Claude Code integration installs skills into `.claude/skills` (see
integrations/claude: `"dir": ".claude/skills"`), and the "what gets kept"
list earlier in this same doc already says `.claude/skills/`. But three
troubleshooting/reference spots still point users at `.claude/commands/`,
which does not exist for a Claude Code install -- so the "verify files
exist" checks list an empty/missing directory. Correct all three to
`.claude/skills/`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:51:40 -05:00
WOLIKIMCHENG
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>
2026-07-24 11:05:32 -05:00
Ali jawwad
71e6201790 fix(auth): normalize whitespace in auth-config env-var/id references at store time (#3691)
* fix(auth): normalize whitespace in auth-config env-var/id references at store time

token_env, client_secret_env, tenant_id, and client_id were VALIDATED on
their .strip()ed form but STORED raw, so an accidentally padded value passed
validation yet silently broke the downstream verbatim os.environ.get(name) /
OAuth-URL lookups — load_auth_config succeeded but resolve_token returned
None and the request quietly downgraded to unauthenticated (401/403) with no
diagnostic.

Normalize these whitespace-insignificant string references with a _norm
helper at store time, mirroring how `hosts` is already normalized
(h.strip().lower()). `token` is unchanged (already stripped at resolve time).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* test(auth): cover tenant_id/client_id/client_secret_env normalization

Address review: the regression test only covered token_env, but the fix also
normalizes tenant_id, client_id, and client_secret_env. Add a padded
azure-ad entry asserting all three are stored stripped.

🤖 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>
2026-07-24 11:03:06 -05:00
Ali jawwad
ea6843c1fe fix(workflows): guard non-mapping 'inputs:' block in engine._resolve_inputs (#3696)
execute()/resume() run UNVALIDATED definitions (load_workflow does not
validate). WorkflowDefinition stores `inputs` raw, so a non-mapping
`inputs:` block (bare `inputs:` -> None, or `inputs: []`) crashed
_resolve_inputs at `for name, input_def in definition.inputs.items()` with
AttributeError, aborting the whole run.

Return {} when inputs is not a mapping, mirroring validate_workflow's own
`isinstance(definition.inputs, dict)` check. Protects both call sites
(execute and resume); normal dict resolution is unchanged.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 10:52:28 -05:00
github-actions[bot]
b0850c97e6 Update Intake Authoring Governance preset to v0.2.0 (#3721)
Update intake-authoring-governance preset submitted by @hindermath to:
- presets/catalog.community.json (version, download_url, documentation, description, provides, tags)
- docs/community/presets.md community presets table

Closes #3720


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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-24 10:51:23 -05:00
Manfred Riem
781a14a6d2 docs: clarify shell-step interpolation safety (#3719)
* docs: clarify shell-step interpolation safety

Shell step `run` fields are executed by the system shell and `{{ ... }}`
expressions are substituted as raw, unquoted text. Document that untrusted
sources — workflow `inputs.*` and prior-step output, including AI-generated
`prompt` output — must be quoted, enum-constrained, validated, or gated before
they reach a `run` field.

- docs/reference/workflows.md: add an "Interpolation and shell safety" section.
- workflows/README.md: add a warning under the Shell Steps example, link to the
  new section, and quote the `inputs.project_dir` example.
- workflows/PUBLISHING.md: strengthen the interpolation guidance and call out
  prior-step/agent output as untrusted.

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

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c0fd6900-69bf-4fcb-b377-de37f98f5835

* docs: correct shell-step interpolation guidance

Address review feedback that the previous wording over-promised. Clarify that
none of the mitigations neutralise a hostile interpolated value:

- Quoting is not a security boundary — there is no shell-escaping filter, and a
  value containing the matching quote can break out. Present quoting as
  correctness handling for already-constrained values only.
- Remove the "pass data via environment or files" guidance: ShellStep has no
  `env` mapping (it only copies the process environment and sets
  SPECKIT_WORKFLOW_DIR), so that transport does not exist.
- Drop the claim that routing through a command/prompt step validates or safely
  binds a value; it does not.
- Correct the gate guidance: a gate renders only its own message/show_file and
  does not inspect, resolve, or sanitise the following step. Authors must
  surface the exact command/data in the gate themselves, and approval does not
  neutralise an injectable interpolation.

Frame constraining values at the source (enum/allowlist) as the only reliable
control, and keeping unconstrained values out of `run` fields entirely.

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

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c0fd6900-69bf-4fcb-b377-de37f98f5835

* docs: remove unsafe interpolation from example and gate guidance

Address further review feedback:

- workflows/README.md: the shell example interpolated an unconstrained path
  into shell source, which contradicted the warning beneath it. Shell steps
  already run from the project root, so drop the `cd '{{ inputs.project_dir }}'`
  prefix and model a plain `run: "npm test"` with no interpolation.
- docs/reference/workflows.md: GateStep prints `message` verbatim with no
  control-character stripping (stripping applies only to `show_file` path and
  contents), so recommending that authors surface untrusted data in `message`
  was itself unsafe — agent/caller output could inject terminal escapes to
  alter or hide the prompt. Direct authors to keep `message` to trusted text
  and surface untrusted material via `show_file`, whose contents are stripped.

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

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c0fd6900-69bf-4fcb-b377-de37f98f5835

* docs: use correct prompt-step output key in example

A `prompt` step stores agent-generated text under `output.stdout`, not
`output.value`, so the example expression `{{ steps.plan.output.value }}`
would resolve to None. Reference `output.stdout` so the example correctly
demonstrates untrusted agent output flowing into a shell step.

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

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c0fd6900-69bf-4fcb-b377-de37f98f5835

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: c0fd6900-69bf-4fcb-b377-de37f98f5835
2026-07-24 10:40:26 -05:00
github-actions[bot]
be0c741ebb [extension] Add Blueprint Index — Living Architecture Map extension to community catalog (#3718)
* Add Blueprint Index extension to community catalog

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

Closes #3628

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

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-24 10:24:19 -05:00
Noor ul ain
36754522f7 fix(github-http): return None on malformed host in resolve_github_release_asset_api_url (#3715)
Accessing the parsed authority (via urlparse/.hostname) raises ValueError
on a malformed bracketed host, e.g. https://[not-an-ip]/..., mirroring
the existing .port guard below. download_url is server-controlled (a
catalog download_url payload), so the function's resolve-or-return-None
contract must hold rather than leaking a raw traceback to the caller.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 09:58:24 -05:00