mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
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>