24 Commits
Author SHA1 Message Date
0fbd99d594 feat(copilot): default integration to skills (#3976)
* feat(copilot): default integration to skills

Make Copilot skills the default while retaining the commands layout behind --integration-options="--commands". Preserve historical project layouts and validate conflicting mode flags before switch teardown.

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

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

Copilot-Session: 930d846b-8921-44ef-9f45-3e77c036b6b5

* fix(copilot): preserve layout state during migration

Keep target integration options isolated from fallback state, prefer the Copilot manifest when resolving layouts, and update dispatch coverage for the skills-first default.

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

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

Copilot-Session: 930d846b-8921-44ef-9f45-3e77c036b6b5

---------

Copilot-Session: 930d846b-8921-44ef-9f45-3e77c036b6b5
2026-08-04 06:15:40 -05:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>CopilotManfred RiemCopilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
8394c8d536 [bug-fix] Fix upgrade-overwrites-copilot-skills: pass force=True to extension skill re-registration after upgrade (#3853)
* Fix upgrade-overwrites-copilot-skills: pass force=True to extension skill re-registration after upgrade

Apply the remediation from the bug assessment on issue #3849.

_register_extension_skills() had a skip guard that refused to overwrite
existing SKILL.md files (protecting user customizations). In the upgrade
path, setup() regenerates all core-template SKILL.md files first, then
calls register_enabled_extensions_for_agent(). The guard then sees those
freshly-written core files as 'existing' and skips every extension, leaving
only core template content on disk.

Fix: add force: bool = False to _register_extension_skills() and thread it
through register_enabled_extensions_for_agent() and
_register_extensions_for_agent(). In integration_upgrade(), pass force=True
so extension content layers on top of the just-regenerated core files.

The force flag is off-by-default so plain extension add still protects
user-modified skill files.

Refs #3849

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

* Potential fix for pull request finding 'Unused local variable'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* test: add end-to-end regression guard for upgrade-overwrites-copilot-skills (#3849)

The existing regression tests in TestRegisterExtensionSkillsForceFlag exercise
the new force parameter at the helper level, so without the fix they fail only
with a TypeError (unknown kwarg) rather than on the user-facing behaviour.

Add a command-level test that runs 'specify integration upgrade copilot --skills
--force' end-to-end and asserts the installed git extension's SKILL.md is
restored (with its extension content, not a bare core-template stub) when the
skill directory already exists — the exact skill_dir_preexists path the bug
depends on. The test fails on pre-fix source (the skill is never recreated) and
passes with the fix, so it is a genuine behavioural regression guard rather than
an API-surface check.

Refs #3849

Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous)

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-07-29 10:03:14 -05:00
Pascal THUETandGitHub 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
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
Quratulain-bilalandGitHub 58b3cadb39 fix(extensions): parse SKILL.md on the --- delimiter line during removal (#3634)
* fix(extensions): parse SKILL.md on the --- delimiter line during removal

ExtensionManager._unregister_extension_skills verified an installed skill
before deleting it by reading metadata.source back from its SKILL.md with a
raw split("---", 2). That substring split stops at the first "---" anywhere
after the opening delimiter, including one embedded in a command description
(e.g. "Separate sections with --- markers"). The frontmatter was then
truncated mid-value, metadata.source parsed empty, the skill looked
unrelated, and its directory was left orphaned on uninstall.

Parse on the "---" delimiter *line* instead, reusing CommandRegistrar.
parse_frontmatter (the line-anchored parser from #3590) in both the fast
(registry-driven) and fallback (directory-scan) removal paths.

Add a regression test that installs an extension whose command description
contains "---", removes it, and asserts the skill directory is gone. Fails
before the fix (dir orphaned), passes after.

* test: cover the fallback scan branch for the --- SKILL.md parse

Copilot noted the new regression test only exercised the fast removal
path (skills_project keeps ai_skills enabled, so remove() resolves the
skills dir directly). Add test_skills_removed_with_dashes_via_fallback_scan,
which deletes init-options.json after install so _get_skills_dir() returns
None and removal takes the fallback directory-scan branch. That branch
re-reads metadata.source with an independently duplicated parser; reverting
it to the old substring split now fails this test (dir orphaned) while the
fast-path test still passes.
2026-07-23 12:49:06 -05:00
74662cffad feat: update Bob integration to skills-based layout for Bob 2.0 (#3415)
* feat: update Bob integration to skills-based layout for Bob 2.0

Bob 2.0 replaces the command-based workflow (.bob/commands/*.md) with
a skills-based layout (.bob/skills/speckit-<name>/SKILL.md), matching
the pattern used by Claude Code, Codex, and other skills-first agents.

- Switch BobIntegration from MarkdownIntegration to SkillsIntegration
- Update folder/dir from .bob/commands to .bob/skills
- Change extension from .md to /SKILL.md (skills layout)
- Add --skills option (default: True) consistent with Codex pattern
- Update tests to inherit from SkillsIntegrationTests (28 tests pass)
- Bump catalog entry to version 2.0.0 with updated description

Assisted-by: IBM Bob (model: claude-sonnet-4-5, autonomous)

* PR comments fix: keep old Bob 1 commands till next release

* Copilot suggested change

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

* feat(bob): address copilot comments, make skills layout default, demote legacy commands to opt-in

* fix(bob): honor legacy_commands in ai_skills persistence and add bob to ALWAYS_SLASH_AGENTS

- init.py: suppress ai_skills=True when --legacy-commands is passed so
  extensions and presets target .bob/commands, not .bob/skills
- _invocation_style.py: add 'bob' to ALWAYS_SLASH_AGENTS so init next-steps
  and hook invocations always show /speckit-<name> (skills is the default
  layout; no ai_skills flag required)

* Copilot suggestion

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

* fix(bob): extend IntegrationBase directly to avoid false isinstance(SkillsIntegration)

- bob/__init__.py: switch BobIntegration base from SkillsIntegration to
  IntegrationBase; add _BobSkillsHelper for skills-mode delegation; set
  invoke_separator='-' explicitly; set _skills_mode flag in setup() so
  consumers can derive the effective mode without isinstance checks
- _helpers.py: replace isinstance(integration, SkillsIntegration) guard
  with getattr(_skills_mode) so legacy-commands mode does not persist
  ai_skills=True
- _invocation_style.py: remove 'bob' from ALWAYS_SLASH_AGENTS — Bob 2.0
  skills are invoked via natural language, not /skill-name slash commands
- integrations/catalog.json: advance updated_at to 2026-07-15

* fix(lint): remove unused SkillsIntegration import from _helpers.py

* Copilot suggested change

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

* feat(bob): add bob skills integration with registrar-based mode detection

* address 3 comments from copilot

* feat(bob): update registrar config to use legacy commands layout

* fix lint

* Suggested fix from Copilot

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

* fix pr comment

* fix pr comment

* fix pr comment

* refactor(bob): resolve skills mode via base-class hooks + fix command-ref separators

Rework the dual-mode handling introduced for Bob 2.0 so an integration's
internal representation never leaks into shared init/install/upgrade code,
and fix the legacy command-reference separator surfaced in review.

Base-class contract:
- Add IntegrationBase.is_skills_mode(parsed_options) — the single hook the
  shared machinery consults to decide whether to persist ai_skills and render
  skill invocations. SkillsIntegration returns True; Copilot honors --skills /
  self._skills_mode; Bob returns `not legacy_commands`.
- Add IntegrationBase.invoke_separator_for_mode(skills_enabled) — resolves the
  command-ref separator from a project's persisted mode for registration paths
  that only have the ai_skills flag (no CLI parsed_options). Default is
  behavior-preserving; Bob maps skills->"-", legacy->".".
- BobIntegration stays on IntegrationBase (mirroring Copilot, the other
  dual-mode agent) and delegates setup() to internal _BobSkillsHelper /
  _BobMarkdownHelper. Removes the _skills_mode method and all
  isinstance(SkillsIntegration) / callable(_skills_mode) probing from
  _helpers.py and init.py.

Fix legacy separator (review feedback): CommandRegistrar.register_commands and
PresetManager._resolve_skill_command_refs previously read the single static
AGENT_CONFIGS[key]["invoke_separator"], so legacy .bob/commands/ extension and
preset command refs rendered /speckit-<cmd> instead of Bob 1.x /speckit.<cmd>.
Both now resolve the separator per project mode via invoke_separator_for_mode.

Tests: add regression coverage for the is_skills_mode / invoke_separator_for_mode
hooks and legacy extension command-ref separators; normalize a width-sensitive
workflow assertion to match its siblings. Full suite green.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf

* fix(bob,copilot): address review — preserve legacy layout, dual-mode separators, extension-skill token resolution

Addresses PR review 4716036212 (3 comments):

1. Bob legacy-install regression: `use`/`switch`/`upgrade` on an existing
   Bob 1.x project (only `.bob/commands/` on disk, no stored
   `legacy_commands`) called `is_skills_mode(None)` -> True and rewrote
   `ai_skills=True`, silently switching extension/command-reference handling
   to the skills layout. `is_skills_mode` now takes an optional `project_root`;
   Bob preserves an already-installed legacy layout until an explicit upgrade
   creates `.bob/skills/`. A fresh project still defaults to skills.

2. Copilot dual-mode separator: `invoke_separator_for_mode` was inherited
   from the base (mode-independent) and returned Copilot's static `.`, so
   preset/extension command refs in a Copilot skills project rendered
   `/speckit.<name>` instead of `/speckit-<name>`. Override it on Copilot to
   track the persisted `ai_skills` state, consistent with
   `build_command_invocation` and `effective_invoke_separator`.

3. Bob extension-skill command-ref tokens: verified that merging main's
   generic `_resolve_command_ref_tokens` (#3544) resolves Bob's tokens via
   the `CONDITIONAL_SLASH_AGENTS` path (`/speckit-<name>`); added Bob to the
   command-ref regression parametrize plus dedicated Bob use-path tests.

All tests pass (full suite green; merged with current main incl. #3544).

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf

* fix(bob): resolve command-ref separator with project-aware mode before shared-infra refresh (review #3415)

The `use`/`switch` paths refresh shared infrastructure via
`_with_integration_setting()` / `_invoke_separator_for_integration()`,
which previously resolved the invoke separator through
`effective_invoke_separator` / `is_skills_mode` WITHOUT a project_root.
For a pre-PR Bob 1.x project (.bob/commands/ on disk, no stored options),
this defaulted to the skills "-" separator and rewrote rendered
shared-template command refs to /speckit-*, even though ai_skills stayed
false. Thread project_root through effective_invoke_separator, the two
runtime helpers, and every call site so Bob's on-disk legacy detection
governs the separator before shared infra is refreshed.

Add a rendered-shared-template regression test covering `use --force`.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf

* fix(bob): scope persisted ai_skills flag to active agent when resolving command-ref separator (review #3415)

`register_commands` runs once per detected agent, but the persisted
`ai_skills` flag describes only the active integration (`opts["ai"]`).
When another agent (e.g. Copilot) is active in skills mode while a
legacy `.bob/commands` layout is also present, the previous code passed
that global `True` to Bob's `invoke_separator_for_mode`, rewriting Bob
1.x command refs to `/speckit-*` instead of `/speckit.*`.

Only consult the persisted flag for the agent it describes
(`opts["ai"] == agent_name`); otherwise resolve the separator from the
agent's own project-aware `effective_invoke_separator(None, project_root)`.

Add regression tests covering the mismatched-active-agent case and a
control for Bob-active skills mode.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf

* fix(bob): detect Spec Kit layout from managed artifacts, not any skills dir (review #3415)

Two related mis-detections from review 4723246468:

1. `BobIntegration.is_skills_mode` treated the mere presence of a
   `.bob/skills/` directory as proof the project is skills-based. A legacy
   Spec Kit install (managed `.bob/commands/speckit.*.md`) that also carried
   unrelated Bob 2 skills would be misclassified as skills, so
   `integration use bob` persisted `ai_skills` and rewrote shared refs.
   Now the layout is inferred from managed Spec Kit artifacts: legacy/command
   mode only when managed `speckit.*.md` command files exist and no managed
   `speckit-*` skill dirs do.

2. The `register_commands` separator for an inactive agent used a disk-based
   `effective_invoke_separator(None, project_root)` fallback that could pick
   the skills separator even though the registrar writes the static command
   layout (`.bob/commands/*.md`). Inactive agents now resolve the separator
   from the registrar's actual output layout (`extension == "/SKILL.md"`),
   so command-layout files keep `/speckit.*` refs regardless of sibling dirs.

Update the affected hook/E2E tests to use managed artifacts and add
regression tests for the mixed-layout and inactive-registrar scenarios.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf

* fix(bob): apply managed-artifact detection on upgrade + consistent skill post-processing (review #3415)

Two issues from review 4723782860:

1. `BobIntegration.setup()` resolved the layout via `is_skills_mode(parsed_options)`
   WITHOUT `project_root`, so `integration upgrade bob` on a Bob 1.x install
   (managed `.bob/commands/speckit.*.md`, no stored options) ignored the
   existing command files, generated skills, and stale-deleted the legacy
   commands — silently migrating the project. Pass `project_root` so the same
   managed-artifact detection used by `use` also governs upgrades.

2. Only `_BobSkillsHelper` overrode `post_process_skill_content` to suppress
   the shared slash-command hook note. Preset/extension skill generators call
   that hook on the registered `BobIntegration`, which inherited
   `IntegrationBase`'s note-injecting default. Repeat the no-op (delegating to
   the skills helper) on the registered class so every Bob skill-generation
   path is consistent with intent-activated core Bob skills.

Add regression tests for the upgrade-preservation and post-processing paths.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf

* feat(bob): add --skills migration opt-in; fix separator + manifest loss (review #3415)

Address review #3415 (4724160183):

- Comment 1: Add an explicit `--skills` opt-in to BobIntegration. It forces
  the skills layout over on-disk auto-detection, giving legacy Bob 1.x
  installs a supported migration path
  (`integration upgrade bob --integration-options="--skills"`). `--skills`
  and `--legacy-commands` are mutually exclusive (clean exit-1 error).

- Comment 2: In CommandRegistrar.register_commands, derive the command-ref
  separator from the output layout (agent_config["extension"]) for the
  active agent too, not the persisted ai_skills flag. A command-layout file
  (.bob/commands/*.md, .github/agents/*.agent.md) always renders /speckit.*;
  only a /SKILL.md scaffold uses /speckit-*. Dual-layout agents (Bob,
  Copilot) write skills via their own setup()/skills path, so
  register_commands only ever emits their command-layout files.

- Comment 3: Update docs/reference/integrations.md Bob entry to document the
  skills-based default (.bob/skills/), the deprecated --legacy-commands
  opt-out, and the --skills migration path.

Also fix a latent manifest-loss bug surfaced by the migration path: the
upgrade Phase 2 stale-file cleanup built a throwaway manifest sharing the
integration key and called uninstall(), which always deleted
{key}.manifest.json. Any layout-shrinking upgrade (e.g. legacy->skills)
thus wiped the freshly-saved manifest, leaving the project untracked and
un-upgradeable. uninstall() now takes remove_manifest (default True); the
stale-cleanup pass passes False.

Adds regression tests for the --skills opt-in, mutual exclusion, corrected
active-agent separator, remove_manifest=False, and an end-to-end
legacy->skills migration that verifies the manifest survives and the
project remains upgradeable. Full suite: 4555 passed, 5 skipped.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf

* docs(agents): align token-resolution comment with output-layout separator rule (review #3415)

Address review #3415 (4725516805). The comment above resolve_command_refs
still described the removed state-based behavior ("resolve it from the
integration using the project's persisted skills state"). Update it to
describe the output-layout rule that register_commands now uses: _sep is
derived from the layout this registrar writes (a /SKILL.md scaffold uses the
skills separator; a command-layout file uses the command separator), not the
persisted ai_skills state. Comment-only change; no behavior change.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf

* fix(bob): reconcile extension artifacts on layout change (review #3415)

When a dual-mode agent (Bob) flips between the legacy commands layout and
the skills layout during `integration upgrade` (via `--skills` /
`--legacy-commands`), the old layout's extension command/skill files were
left orphaned: Phase 2 stale cleanup only removes files tracked by the
*integration* manifest, while extension artifacts are tracked in the
extension registry. Detect the layout flip by comparing whether the old vs
new manifest tracks a `/SKILL.md` scaffold, and when it changed, unregister
the agent's extension artifacts before the existing re-registration so they
are recreated in the new layout (and the per-agent registry is updated).

Preset artifacts are documented as a known, pre-existing cross-cutting gap:
no agent-scoped preset re-registration exists in use/switch/upgrade for any
agent, so reconciling them is out of scope for this Bob migration.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf

* fix(bob): reject layout migration when preset overrides are installed (review #3415)

A command↔skills layout change during `integration upgrade` cannot reconcile
preset artifacts: presets track their command/skill files in per-preset
`registered_commands`/`registered_skills` metadata, and there is no
agent-scoped preset re-registration anywhere in the CLI. Migrating would
delete a preset's old-layout files without recreating them in the new layout
and leave the preset registry claiming artifacts that no longer exist.

Detect the intended layout via `is_skills_mode` (so a plain same-layout
upgrade is unaffected) and, when it flips while preset overrides are
installed for the agent, reject the upgrade *before any mutation* with an
actionable error pointing at the remove → upgrade → reinstall workaround.
Extension artifacts are still reconciled for the safe (no-preset) case.

Adds a regression test and documents the migration caveat in the Bob
integration reference entry.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf

* fix(bob): restrict layout reconciliation to the active integration (review #3415)

`integration_upgrade` supports upgrading a secondary (non-active) integration,
but the layout-change extension reconciliation was unsafe there.
`ExtensionManager.unregister_agent_artifacts()` treats the unscoped
per-extension `registered_skills` list as belonging to the passed agent and,
when that agent's skills directory is absent, falls back to scanning every
agent's skills directory — so reconciling a secondary Bob layout flip could
delete or untrack the *active* agent's extension skills. The subsequent
re-registration cannot repair that because extension skill rendering is
intentionally scoped to the active agent (#2948).

Gate the unregister-before-register reconciliation on `installed_key == key`
so it only runs for the active integration. Secondary agents only ever have
extension command files (skills are active-agent-only), which the existing
re-registration rewrites in place, so skipping the unregister orphans nothing
new. Adds a regression test asserting a secondary Bob layout change leaves the
active agent's extension skill intact on disk and in the registry.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf

* fix(bob): fail closed when preset registry is unreadable (review #3415)

Address review 4744636079:

- _migrate_commands: the preset guard previously failed *open* — a
  registry read/parse error returned an empty "no presets" list, so a
  --force layout-changing upgrade could delete preset-overridden command
  files while their registry state was unknown. Read the registry file
  directly and raise _PresetRegistryUnreadableError on any read/parse
  failure or malformed structure, rejecting the migration before any
  mutation. A genuinely absent registry still returns [] (safe).

- bob: correct the is_skills_mode docstring — upgrade *does* run setup();
  disk detection is needed because legacy Bob 1.x installs never persisted
  a legacy_commands option, so the stored mode is unavailable.

- tests: add fail-closed E2E (corrupted registry rejected, valid-empty
  allowed) plus a unit test for _installed_presets_affecting_agent covering
  absent / corrupted / malformed / valid / affecting-agent cases.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf

* fix(bob): fail closed on malformed preset entries too (review #3415)

Address review 4745191015: the preset guard read a parseable registry but
silently skipped malformed per-preset metadata and treated a malformed
registered_commands value as "no matching artifacts". A registry such as
{"presets":{"p1":[]}} therefore allowed a layout migration even though p1's
ownership is unknown, risking deletion of preset-managed files. Now raise
_PresetRegistryUnreadableError for a non-dict preset entry, a non-dict
registered_commands, or a non-list registered_skills. Extend the unit test
to cover these malformed shapes.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63f93544-a77f-4f01-bf04-c88806a97dbf

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-07-21 09:20:20 -05:00
4fed84a08d fix(extensions): resolve __SPECKIT_COMMAND tokens in auto-registered skills (#3544)
* fix(extensions): resolve command ref tokens in extension skills

* fix(extensions): render skill command refs by invocation style

Resolve extension skill command-reference tokens with the active skill invocation style so Codex and ZCode use $speckit-* while slash-style agents keep their native forms. Preserve literal command-looking text.

* fix(extensions): resolve slash skill command refs from init options

---------

Co-authored-by: root <kinsonnee@gmail.com>
2026-07-16 07:21:14 -05:00
3b7d95a408 fix: rewrite extension-relative subdir paths in generated command bodies (#3444)
* fix: rewrite extension-relative subdir paths in generated command bodies

Extension command bodies reference bundled files relative to the
extension root (agents/, knowledge-base/, templates/, ...). Generated
SKILL.md and command files emitted those paths verbatim, so agents
resolved them against the workspace root where they do not exist.

Add CommandRegistrar.rewrite_extension_paths, which rewrites references
to subdirectories that actually exist in the installed extension to
.specify/extensions/<id>/..., and call it once in register_commands so
every output format and alias gets the fix. commands/, specs/ and
dot-directories are never rewritten.

Fixes #2101

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

* fix: only rewrite relative extension path references

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

* fix: use callable re.sub replacement for extension subdir rewrite

subdir and extension_id come from filesystem directory names and were
interpolated into a re.sub string replacement template. A directory name
containing a backslash (e.g. assets\q) would raise re.error: bad escape,
aborting command registration even when the body didn't reference it.
Use a callable replacement so these values are treated literally.

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

* fix: make subdir rewrite regression test cross-platform

Renamed the test's subdir fixture from "assets\\q" to "assets[q]":
on Windows, backslash is a path separator, so mkdir would create
nested "assets/q" dirs instead of one literally-named directory,
and iterdir() would only discover "assets", never exercising the
rewrite. extension_id keeps a real backslash/"\\1" since it isn't
used to create a directory, still verifying the callable replacement
handles it literally. Added a sanity assertion for this assumption.

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

* fix: apply extension subdir path rewrite in skills-mode renderer

register_commands() rewrote extension-relative subdir references
(agents/, knowledge-base/, etc.) via rewrite_extension_paths(), but
_register_extension_skills() - the separate renderer used for active
non-native skills agents (e.g. Claude with ai_skills: true) - never
called it. Generated SKILL.md files left agents/... and
knowledge-base/... unresolved, and mapped the extension's own
templates/ through the generic project-level rewrite instead of its
installed .specify/extensions/<id>/templates/ location.

Reuse the existing rewrite_extension_paths() helper in
_register_extension_skills() at the same point register_commands()
applies it (before resolve_skill_placeholders' generic rewrite), and
add a skills-mode regression test.

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

* fix: apply extension subdir path rewrite on preset restore/reconcile paths

_unregister_skills() restored extension-backed SKILL.md content via
resolve_skill_placeholders() without first calling
rewrite_extension_paths(), so removing a preset override that shadowed
an extension command restored the bare, unresolvable agents/... and
knowledge-base/... references. Carried extension_id/extension_dir
through _build_extension_skill_restore_index() and applied the same
rewrite used at initial registration before restoring.

Found the identical gap in _reconcile_composed_commands()'s non-skill
agent path: when a removed preset's command reverts to an extension
winner, register_commands_for_non_skill_agents() was called without
extension_id, so the rewrite never ran for plain command-file agents
either. Passed extension_id through there too.

Added regression tests for both restore paths (skills-mode and
non-skill-agent command files) in tests/test_presets.py.

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

* fix: apply extension subdir path rewrite when composing over extension base

PresetResolver.resolve_content() read the effective base layer's raw
content directly via path.read_text() before composing append/prepend/
wrap overlays on top of it, and its outright-replace shortcut did the
same. When that base layer was extension-provided, neither read path
applied rewrite_extension_paths(), so composing a preset over an
extension command (or an extension winning outright through
resolve_content) left bare, unresolvable agents/... and
knowledge-base/... references in the composed output.

All three call sites (PresetManager._register_commands()'s composed
path, _reconcile_composed_commands()'s composed path, and skills-mode
reading the .composed file written by either) consume resolve_content's
return value, so fixing the read at its source covers command output,
skill output, and both initial-install and reconcile flows without
threading extension identity through each caller.

Tagged extension layers in collect_all_layers() with extension_id/
extension_dir, and added a _read_layer_content() helper in
resolve_content() that applies rewrite_extension_paths() whenever a
layer carries that extension identity — used at both raw-read sites
(outright-replace shortcut and composition base). Composing
(append/prepend/wrap) layers are never extension-provided (extensions
are always inserted with strategy "replace"), so no other read site
needs the rewrite.

Added regression tests: a parametrized resolve_content() test covering
append/prepend/wrap composing over an extension base, a skills-mode
test asserting the composed SKILL.md resolves the extension's subdir
references, and a non-skill-agent (Gemini) install-time test matching
the reported live repro.

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

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-13 10:50:59 -05:00
1930f89d17 fix extension-local script path rewriting (#3364)
Co-authored-by: Zhiyao <zhiyao@ZhiyaodeMacBook-Air.local>
2026-07-07 10:51:50 -05:00
JinHyuk SungandGitHub 0c975bbef7 fix: write Codex dev skills as files (#2988)
* fix: write Codex dev skills as files

* fix: route codex dev symlink policy through metadata

* fix: replace codex dev symlinks on refresh

* fix: migrate codex dev skill symlinks

* fix: avoid inactive shared skill dev symlinks

* fix: preserve unrelated dev skill symlinks
2026-06-23 10:55:38 -05:00
Pascal THUETandGitHub d9370d909d fix: isolate per-extension failures so one bad extension can't drop the rest (#2951)
* fix: isolate per-extension failures in register_enabled_extensions_for_agent

The per-extension loop had no error isolation: if registering one enabled
extension raised (e.g. an OSError writing a command file), the loop aborted and
the exception propagated, so every subsequent enabled extension was silently
skipped. Callers wrap the whole call in a single best-effort try/except, so the
wholesale abort surfaced as one warning while the command still exited 0 —
leaving the agent with only a prefix of its extensions.

Wrap the per-extension body in try/except: warn (naming the extension) and
continue, so one bad extension can no longer drop the others. Add a regression
test that forces the first-iterated extension to raise and asserts the rest
still register.

Closes #2950

* fix(extensions): preserve command registry when skills fail

* fix: clarify skill registration warning
2026-06-19 12:41:02 -05:00
811a3aa447 fix(skills): preserve non-ASCII characters in skill frontmatter (#2917)
* fix(skills): preserve non-ASCII chars in skill frontmatter

Skill SKILL.md frontmatter descriptions containing non-ASCII
characters were escaped to \uXXXX / \xXX sequences because
yaml.safe_dump() was called without allow_unicode=True.

- Add allow_unicode=True to the 7 skill/command frontmatter
  safe_dump sites (extensions, presets, claude integration)
- Add regression tests for the render and extension-install paths

Follows the approach of #1936; encoding="utf-8" is already set on
the affected write paths, so no encoding change is needed here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(_utils): add dump_frontmatter helper

Centralize skill/command frontmatter YAML serialization into a single
_utils.dump_frontmatter helper so no call site can drop allow_unicode or
diverge on formatting. Route the 7 existing sites through it and drop a
now-unused local yaml import.

Switch the extension test fixtures to yaml.safe_dump for parity with the
production safe-dump/safe-load codepaths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 07:57:54 -05:00
63a2a17305 fix(extensions): preserve argument-hint in extension Claude SKILL.md (#2916)
Extension-provided commands that declare `argument-hint:` in their
frontmatter had that field dropped from the generated Claude
`.claude/skills/<name>/SKILL.md`, while core template commands keep it.
The extension skill generator built the frontmatter via the shared
build_skill_frontmatter() (name/description/compatibility/metadata only)
and never forwarded argument-hint.

Carry argument-hint from the parsed source command frontmatter into the
skill frontmatter dict before serialization, gated on the integration
exposing inject_argument_hint so only argument-hint-aware agents (Claude)
receive the key and build_skill_frontmatter's shared shape stays unchanged
for every other agent. The value is injected into the dict rather than via
the string-based inject_argument_hint helper, so a folded multi-line
description cannot be split into invalid YAML.

Add regression tests covering a folding description (Claude) and the
non-Claude gate (kimi).

Closes #2903

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 07:28:39 -05:00
CopilotGitHubCopilotcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Manfred Riem
7106858c4e feat!: remove legacy --ai, --ai-commands-dir, and --ai-skills flags (0.10.0) (#2872)
* Initial plan

* feat!: remove legacy --ai, --ai-commands-dir, and --ai-skills flags at 0.10.0

* refactor(tests): rename stale test_ai_help_* methods to test_agent_config_*

* fix: address review — derive agent folder for generic integration and remove redundant test

- Security notice now falls back to integration_parsed_options['commands_dir']
  when AGENT_CONFIG folder is None (generic integration).
- Remove test_agent_config_includes_kiro_cli which duplicates the assertion
  in test_runtime_config_uses_kiro_cli_and_removes_q.

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

* docs: scrub all remaining --ai flag references from source and tests

- Remove dead AI_ASSISTANT_ALIASES, AI_ASSISTANT_HELP, and
  _build_ai_assistant_help() from _agent_config.py
- Update comments/docstrings in extensions.py, presets.py, and
  integration subpackages to reference 'skills mode' or
  '--integration' instead of the removed flags
- Fix catalog.json generic integration description
- Update test docstrings/comments in test_extension_skills.py,
  test_extensions.py, and test_presets.py

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

* test: remove legacy --ai flag rejection tests

The flags are fully removed from the CLI; typer handles unknown options
generically. No custom rejection logic exists to test.

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

* revert: remove manual CHANGELOG.md entry

CHANGELOG is generated automatically; manual edits should not be made.

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

* fix: make generic catalog description self-explanatory

Include the required --commands-dir sub-option in the description so
readers don't need to look up integration docs.

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

* fix(tests): rename duplicate test classes to avoid shadowing

The rename from Test*AutoPromote to Test*Integration collided with the
existing Test*Integration(SkillsIntegrationTests) base classes, causing
the shared test suites to be silently overwritten. Rename the CLI init
flow classes to Test*InitFlow instead.

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Manfred Riem <mnriem@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-05 14:56:28 -05:00
a9a759450d fix: recover active skills registration for extensions (#2803)
Extension command registration now resolves the active skills directory before writing command artifacts. This lets initialized skills-backed agents recover a missing active skills directory while preserving the existing preset registration behavior.

Add regression coverage for missing active skills directories, shared skills directories, and symlinked parent guards.

Fixes #2769.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-06-04 09:53:31 -05:00
darion-yaphetandGitHub 67fecd357a chore(tests): fix ruff lint violations in tests/ (#2827)
Clear pre-existing lint debt flagged by repo-wide `ruff check` (the lint
config only scopes src/, so tests/ had drifted). No behavior change.

- F401/F541: drop unused imports and redundant f-string prefixes (autofix)
- E741: rename ambiguous `l` to `ln` in comprehensions
- E702: split semicolon-joined statements onto separate lines
- F841: drop unused bindings while keeping the side-effecting calls
  (_minimal_feature, install_from_directory)

Full suite: 3344 passed, 40 skipped. ruff check (repo-wide): clean.
2026-06-03 16:02:26 -05:00
NgoQuocViet2001andGitHub 9307093d8a Fix --dev extension agent symlinks (#2554)
* Fix dev extension agent symlinks

* Address dev symlink review feedback

* fix: handle dev symlink relpath failures

* fix: fall back when dev cache writes fail

* test: cover dev symlink fallback without privileges
2026-05-28 09:29:17 -05:00
Manfred RiemandGitHub e54653efcc fix: create skills directory on demand during extension/preset install (#2711)
* fix: create skills directory on demand during extension/preset install

_get_skills_dir() in both extensions.py and presets.py returned None
when the skills directory did not yet exist on disk, even though skills
were enabled in init-options. This caused extension skill registration
to silently produce an empty registered_skills list and skip writing
SKILL.md files.

Replace the is_dir() bail-out with mkdir(parents=True, exist_ok=True)
so the directory is created on demand when ai_skills is enabled.

Update the existing test expectation and add a parametrized regression
test (claude + codex) that installs an extension before the skills
directory exists and asserts SKILL.md files and registry entries are
created.

Fixes #2682

* test: assert skills dir is NOT created when skills are disabled

Strengthen negative tests to verify _get_skills_dir does not create the
directory on disk when ai_skills is false or init-options.json is absent.

* fix: add symlink/containment check and preserve Kimi existence gate

Address PR review feedback:

- Use _ensure_safe_shared_directory() instead of raw mkdir() to prevent
  symlink-following writes outside the project root.
- Restore the is_dir() existence gate for the Kimi native-skills
  fallback (ai_skills=false): only create the directory on demand when
  ai_skills is explicitly enabled.
- Update docstrings to reflect the on-demand vs existence-gate behavior.
- Reuse resolve_skills_dir helper in tests instead of manually
  reconstructing paths from AGENT_CONFIG.

* refactor: extract resolve_active_skills_dir shared helper

Deduplicate the _get_skills_dir logic that was nearly identical in
ExtensionManager and PresetManager into a single module-level
resolve_active_skills_dir() function in __init__.py.

The shared helper wraps _ensure_safe_shared_directory errors with
skills-specific messages so users see 'agent skills directory' instead
of 'shared infrastructure directory' in error output.

Both class methods now delegate to the shared helper.

* fix: preserve original error reason in skills dir safety check

Include the original exception message from _ensure_safe_shared_directory
in the re-raised ValueError so the user sees the specific reason (symlink,
not-a-directory, path escape, etc.) instead of a generic message.

* fix: handle skills dir safety errors gracefully during install

Catch ValueError/OSError from _get_skills_dir() inside
_register_extension_skills() so a symlink or permission error logs a
warning and returns [] instead of aborting mid-install and leaving a
partially-installed extension without a registry entry.

Also document OSError in resolve_active_skills_dir() docstring.

* fix: catch errors in _get_skills_dir and use _print_cli_warning

Move the ValueError/OSError catch from _register_extension_skills into
_get_skills_dir itself so all callers (install, uninstall, reconcile)
are protected from unsafe-path exceptions.

Replace logging.getLogger().warning with _print_cli_warning for
consistent Rich-formatted user output.

* fix: use context-aware error messages for skills directory safety

Add a 'context' parameter to _ensure_safe_shared_directory (defaults to
'shared infrastructure directory' for backward compat). The skills dir
caller passes context='agent skills directory' so error messages say
e.g. 'Refusing to use symlinked agent skills directory' instead of
'Refusing to use symlinked shared infrastructure directory'.

Simplify resolve_active_skills_dir by removing the now-unnecessary
try/except wrapper.

* fix: validate Kimi native-skills directory for symlink/containment

The Kimi fallback path (ai_skills=false) used is_dir() which follows
symlinks, so a symlinked .kimi/skills could cause writes outside the
project root. Now validates with _ensure_safe_shared_directory(create=
False) before returning the directory.
2026-05-26 15:10:59 -05:00
Manfred RiemandGitHub fc3d1244c0 fix: replace shell-based context updates with marker-based upsert (#2259)
* Replace shell-based context updates with marker-based upsert

Replace ~3500 lines of bash/PowerShell agent context update scripts
with a Python-based approach using <!-- SPECKIT START/END --> markers.

IntegrationBase now manages the agent context file directly:
- upsert_context_section(): creates or updates the marked section at
  init/install/switch time with a directive to read the current plan
- remove_context_section(): removes the section at uninstall, deleting
  the file only if it becomes empty
- __CONTEXT_FILE__ placeholder in command templates is resolved per
  integration so the plan command references the correct agent file
- context_file is persisted in init-options.json for extension access

The plan command template instructs the LLM to update the plan
reference between the markers in the agent context file.

Removed:
- scripts/bash/update-agent-context.sh (857 lines)
- scripts/powershell/update-agent-context.ps1 (515 lines)
- 56 integration wrapper scripts (update-context.sh/.ps1)
- templates/agent-file-template.md
- agent_scripts frontmatter key and {AGENT_SCRIPT} replacement logic
- update-context reference from integration.json
- tests/test_cursor_frontmatter.py (tested deleted scripts)

Added:
- upsert/remove context section methods on IntegrationBase
- __CONTEXT_FILE__ placeholder support in process_template()
- context_file field in init-options.json (init/switch/uninstall)
- Per-integration tests: context file correctness, plan reference,
  init-options persistence (78 new context_file tests)
- End-to-end CLI validation across all 28 integrations

* fix: search for end marker after start marker in context section methods

Address Copilot review: content.find(CONTEXT_MARKER_END) searched from
the start of the file rather than after the located start marker. If
the file contained a stray end marker before the start marker, the
wrong slice could be replaced.

Now both upsert_context_section() and remove_context_section() pass
start_idx as the second argument to find() and validate end_idx >
start_idx before performing the replacement.

* fix: address Copilot review feedback on context section handling

1. Fix grammar in _build_context_section() directive text — add commas
   for a complete sentence.

2. Resolve __CONTEXT_FILE__ in resolve_skill_placeholders() — skills
   generated via extensions/presets for codex/kimi now replace the
   placeholder using the context_file value from init-options.json.

3. Handle Cursor .mdc frontmatter — when creating a new .mdc context
   file, prepend alwaysApply: true YAML frontmatter so Cursor
   auto-loads the rules.

4. Fix empty-file leading newline — when the context file exists but
   is empty, write the section directly instead of prepending a blank
   line.

* fix: address second round of Copilot review feedback

1. Ensure .mdc frontmatter on existing files — upsert_context_section()
   now checks for missing YAML frontmatter on .mdc files during updates
   (not just creation), so pre-existing Cursor files get alwaysApply.

2. Guard against context_file=None — use 'or ""' instead of a default
   arg so explicit null values in init-options.json don't cause a
   TypeError in str.replace().

3. Clean up .mdc files on removal — remove_context_section() treats
   files containing only the Speckit-generated frontmatter block as
   empty, deleting them rather than leaving orphaned frontmatter.

* fix: address third round of Copilot review feedback

1. CRLF-safe .mdc frontmatter check — use lstrip().startswith('---')
   instead of startswith('---\n') so CRLF files don't get duplicate
   frontmatter.

2. CRLF-safe .mdc removal check — normalize line endings before
   comparing against the sentinel frontmatter string.

3. Call remove_context_section() during integration_uninstall() — the
   manifest-only uninstall was leaving the managed SPECKIT markers
   behind in the agent context file.

4. Fix stale docstring — remove 'agent_scripts' mention from
   test_lean_commands_have_no_scripts().

* fix: address fourth round of Copilot review feedback

1. Remove unused script_type parameter from _write_integration_json()
   and all 3 call sites — the parameter was no longer referenced after
   the update-context script removal.

2. Fix _build_context_section() docstring — correct example path from
   '.specify/plans/plan.md' to 'specs/<feature>/plan.md'.

3. Improve .mdc frontmatter-only detection in remove_context_section()
   — use regex to match any YAML frontmatter block (not just the exact
   Speckit-generated one), so .mdc files with additional frontmatter
   keys are also cleaned up when no body content remains.

* fix: handle corrupted markers and parse .mdc frontmatter robustly

1. Handle partial/corrupted markers in upsert_context_section() —
   if only the START marker exists (no END), replace from START
   through EOF. If only the END marker exists, replace from BOF
   through END. This keeps upsert idempotent even when a user
   accidentally deletes one marker.

2. Parse .mdc YAML frontmatter properly — new _ensure_mdc_frontmatter()
   helper parses existing frontmatter and ensures alwaysApply: true is
   set, rather than just checking for the --- delimiter. Handles
   missing frontmatter, existing frontmatter without alwaysApply, and
   already-correct frontmatter.

* fix: preserve .mdc frontmatter, add tests, clean up on switch

1. Rewrite _ensure_mdc_frontmatter() with regex — preserves comments,
   formatting, and custom keys in existing frontmatter instead of
   destructively re-serializing via yaml.safe_dump(). Inserts or
   fixes alwaysApply: true in place.

2. Add 6 focused .mdc frontmatter tests to cursor-agent test file:
   new file creation, missing frontmatter, preserved custom keys,
   wrong alwaysApply value, idempotent upserts, removal cleanup.

3. Call remove_context_section() during integration switch Phase 1 —
   prevents stale SPECKIT markers from being left in the old
   integration's context file. Also clear context_file from
   init-options during the metadata reset.

* fix: remove unused MDC_FRONTMATTER, preserve inline comments, normalize bare CR

1. Remove unused MDC_FRONTMATTER class variable — dead code after
   _ensure_mdc_frontmatter() was rewritten with regex.

2. Preserve inline comments when fixing alwaysApply — the regex
   substitution now captures trailing '# comment' text and keeps it.

3. Normalize bare CR in upsert_context_section() — match the
   behavior of remove_context_section() which already normalizes
   both CRLF and bare CR.

4. Clarify .mdc removal comment — 'treat frontmatter-only as empty'
   instead of misleading 'strip frontmatter'.

* fix: handle corrupted markers in remove, CRLF-safe end-marker consumption

1. Handle corrupted markers in remove_context_section() — mirror
   upsert's behavior: start-only removes start→EOF, end-only removes
   BOF→end. Previously bailed out leaving partial markers behind.

2. CRLF-safe end-marker consumption — both upsert and remove now
   handle \r\n after the end marker, not just \n. Prevents extra
   blank lines at replacement boundaries in CRLF files.

3. Clarify path rule in plan template — distinguish filesystem
   operations (absolute paths) from documentation/agent context
   references (project-relative paths).

* fix: only remove context section when both markers are well-ordered

remove_context_section() previously treated mismatched markers as
corruption and aggressively removed from BOF→end-marker or
start-marker→EOF, which could delete user-authored content if only
one marker remained. Now it only removes when both START and END
markers exist and are properly ordered, returning False otherwise.
2026-04-17 13:57:51 -05:00
Manfred RiemandGitHub 8fc2bd3489 fix: allow Claude to chain skills for hook execution (#2227)
* fix: allow Claude to chain skills for hook execution (#2178)

- Set disable-model-invocation to false so Claude can invoke extension
  skills (e.g. speckit-git-feature) from within workflow skills
- Inject dot-to-hyphen normalization note into Claude SKILL.md hook
  sections so the model maps extension.yml command names to skill names
- Replace Unicode checkmark with ASCII [OK] in auto-commit scripts to
  fix PowerShell encoding errors on Windows
- Move Claude-specific frontmatter injection to ClaudeIntegration via
  post_process_skill_content() hook on SkillsIntegration, wired through
  presets and extensions managers
- Add positive and negative tests for all changes

Fixes #2178

* refactor: address PR review feedback

- Preserve line-ending style (CRLF/LF) in _inject_hook_command_note
  instead of always inserting \n, matching the convention used by other
  injection helpers in the same module.

- Extract duplicated _post_process_skill() from extensions.py and
  presets.py into a shared post_process_skill() function in agents.py.
  Both modules now import and call the shared helper.

* fix: match full hook instruction line in regex

The regex in _inject_hook_command_note only matched lines ending
immediately after 'output the following', but the actual template
lines continue with 'based on its `optional` flag:'. Use [^\r\n]*
to capture the rest of the line before the EOL.

* refactor: use integration object directly for post_process_skill_content

Instead of a free function in agents.py that re-resolves the
integration by key, callers in extensions.py and presets.py now
resolve the integration once via get_integration() and call
integration.post_process_skill_content() directly. The base
identity method lives on SkillsIntegration.
2026-04-15 14:35:05 -05:00
Manfred RiemandGitHub b1832c9477 Stage 6: Complete migration — remove legacy scaffold path (#1924) (#2063)
* Stage 6: Complete migration — remove legacy scaffold path (#1924)

Remove the legacy GitHub download and offline scaffold code paths.
All 26 agents now use the integration system exclusively.

Code removal (~1073 lines from __init__.py):
- download_template_from_github(), download_and_extract_template()
- scaffold_from_core_pack(), _locate_release_script()
- install_ai_skills(), _get_skills_dir (restored slim version for presets)
- _has_bundled_skills(), _migrate_legacy_kimi_dotted_skills()
- AGENT_SKILLS_MIGRATIONS, _handle_agent_skills_migration()
- _parse_rate_limit_headers(), _format_rate_limit_error()
- Three-way branch in init() collapsed to integration-only

Config derivation (single source of truth):
- AGENT_CONFIG derived from INTEGRATION_REGISTRY (replaced 180-line dict)
- CommandRegistrar.AGENT_CONFIGS derived from INTEGRATION_REGISTRY (replaced 160-line dict)
- Backward-compat constants kept for presets/extensions: SKILL_DESCRIPTIONS,
  NATIVE_SKILLS_AGENTS, DEFAULT_SKILLS_DIR

Release pipeline cleanup:
- Deleted create-release-packages.sh/.ps1 (948 lines of ZIP packaging)
- Deleted create-github-release.sh, generate-release-notes.sh
- Deleted simulate-release.sh, get-next-version.sh, update-version.sh
- Removed .github/workflows/scripts/ directory entirely
- release.yml is now self-contained: check, notes, release all inlined
- Install instructions use uv tool install with version tag

Test cleanup:
- Deleted test_ai_skills.py (tested removed functions)
- Deleted test_core_pack_scaffold.py (tested removed scaffold)
- Cleaned test_agent_config_consistency.py (removed 19 release-script tests)
- Fixed test_branch_numbering.py (removed dead monkeypatches)
- Updated auto-promote tests (verify files created, not tip messages)

1089 tests pass, 0 failures, ruff clean.

* fix: resolve merge conflicts with #2051 (claude as skills)

- Fix circular import: move CommandRegistrar import in claude
  integration to inside method bodies (was at module level)
- Lazy-populate AGENT_CONFIGS via _ensure_configs() to avoid
  circular import at class definition time
- Set claude registrar_config to .claude/commands (extension/preset
  target) since the integration handles .claude/skills in setup()
- Update tests from #2051 to match: registrar_config assertions,
  remove --integration tip assertions, remove install_ai_skills mocks

1086 tests pass.

* fix: properly preserve claude skills migration from #2051

Restore ClaudeIntegration.registrar_config to .claude/skills (not
.claude/commands) so extension/preset registrations write to the
correct skills directory.

Update tests that simulate claude setup to use .claude/skills and
check for SKILL.md layout. Some tests still need updating for the
full skills path — 10 remaining failures from the #2051 test
expectations around the extension/preset skill registration flow.

WIP: 1076/1086 pass.

* fix: properly handle SKILL.md paths in extension update rollback and tests

Fix extension update rollback using _compute_output_name() for SKILL.md
agents (converts dots to hyphens in skill directory names). Previously
the backup and cleanup code constructed paths with raw command names
(e.g. speckit.test-ext.hello/SKILL.md) instead of the correct computed
names (speckit-test-ext-hello/SKILL.md).

Test fixes for claude skills migration:
- Update claude tests to use .claude/skills paths and SKILL.md layout
- Use qwen (not claude) for skills-guard tests since claude's agent dir
  IS the skills dir — creating it triggers command registration
- Fix test_extension_command_registered_when_extension_present to check
  skills path format

1086 tests pass, 0 failures, ruff clean.

* fix: address PR review — lazy init, assertions, deprecated flags

- _ensure_configs(): catch ImportError (not Exception), don't set
  _configs_loaded on failure so retries work
- Move _ensure_configs() before unregister loop (not inside it)
- Module-level try/except catches ImportError specifically
- Remove tautology assertion (or True) in test_extensions.py
- Strengthen preset provenance assertion to check source: field
- Mark --offline, --skip-tls, --debug, --github-token as hidden
  deprecated no-ops in init()

1086 tests pass.

* fix: remove deleted release scripts from pyproject.toml force-include

Removes force-include entries for create-release-packages.sh/.ps1
which were deleted but still referenced in [tool.hatch.build].
2026-04-02 12:34:34 -05:00
Andrii FurmanetsandGitHub a858c1d6da Install Claude Code as native skills and align preset/integration flows (#2051)
* Use Claude skills for generated commands

* Fix Claude integration and preset skill flows

* Group Claude tests in integration suite

* Align Claude skill frontmatter across generators

* Fix native skill preset cleanup

* Keep legacy AI skills test on legacy path

* Move Claude here-mode test to CLI suite
2026-04-02 09:44:48 -05:00
Hamilton SnowandGitHub ccc44dd00a Unify Kimi/Codex skill naming and migrate legacy dotted Kimi dirs (#1971)
* fix: unify hyphenated skills and migrate legacy kimi dotted dirs

* fix: preserve legacy kimi dotted preset skill overrides

* fix: migrate kimi legacy dotted skills without ai-skills flag

* fix: harden kimi migration and cache hook init options

* fix: apply kimi preset skill overrides without ai-skills flag

* fix: keep sequential branch numbering beyond 999

* test: align kimi scaffold skill path with hyphen naming

* chore: align hook typing and preset skill comment

* fix: restore AGENT_SKILLS_DIR_OVERRIDES compatibility export

* refactor: remove AGENT_SKILLS_DIR_OVERRIDES and update callers

* fix(ps1): support sequential branch numbers above 999

* fix: resolve preset skill placeholders for skills agents

* Fix legacy kimi migration safety and preset skill dir checks

* Harden TOML rendering and consolidate preset skill restore parsing

* Fix PowerShell overflow and hook message fallback for empty invocations

* Restore preset skills from extensions

* Refine preset skill restore helpers

* Harden skill path and preset checks

* Guard non-dict init options

* Avoid deleting unmanaged preset skill dirs

* Unify extension skill naming with hooks

* Harden extension native skill registration

* Normalize preset skill titles
2026-03-26 10:53:30 -05:00
DhilipandGitHub 36019ebf1b feat: Auto-register ai-skills for extensions whenever applicable (#1840)
* feat: Auto-register ai-skills for extensions whenever applicable

* fix: failing test

* fix: address copilot review comments – path traversal guard and use short_name in title

* fix: address remaining copilot review comments – is_file guard, skills type-validation, and exact extension ownership check on fallback rmtree

* fix: address copilot round-3 comments – align skill naming with presets.py convention, safe rmdir on fail, require SKILL.md for fallback rmtree, normalize skill_count in CLI

* fix: is_dir() guard in fast-path rmtree and fix ghost-skill assertion naming

* fix: path-traversal guard on skill_name in both rmtree paths of _unregister_extension_skills

* fix: add SKILL.md ownership check to fast-path rmtree and alias shadowed _get_skills_dir import
2026-03-25 07:48:36 -05:00