Mission Specification: Codex .agents/skills manifest-drift idempotency
Mission Branch: issue-702-codex-skills-drift Created: 2026-10-12 Status: Draft Input: GitHub issue #702 — "Codex integration partially fixed; generated .agents/skills state still drifts from manifests/docs" (P1, milestone 4.0.0 release scope).
User Scenarios & Testing (mandatory)
The .agents/skills/spec-kitty.<command>/SKILL.md command-skill projection (consumed by codex/vibe/pi/letta) is declared authority=DERIVED, git_class=IGNORED by the state contract (src/specify_cli/state/contract.py — shared_skills_projection). It is never committed and is always safe to regenerate from the canonical render. The tracked manifest .kittify/command-skills-manifest.json records the hash of that ignored output. Across two checkouts of one project, the tracked manifest and the ignored projection legitimately disagree: an upgrade in checkout A rewrites the manifest hash, but the ignored bytes never travel to checkout B. The command-skill installer then reads checkout B's untouched generated output as a drifted user edit and refuses to regenerate it, so .agents/skills stays out of sync with the manifest after upgrade/sync/init.
User Story 1 - Idempotent regeneration of a drifted Codex command-skill projection (Priority: P1)
An operator runs spec-kitty upgrade (or sync/re-init) on a checkout whose generated .agents/skills/spec-kitty.<command>/SKILL.md bytes match neither the recorded manifest hash nor the current render. They expect the derived projection to be regenerated so the generated state matches the manifest — not kept as a phantom "local edit."
Why this priority: This is the operator-facing defect the issue was raised to P1 for (operator direction, 2026-10-05). An unattended upgrade on a project nobody edited leaves .agents/skills drifted and doctor skills red, with no automatic path back to a coherent state.
Independent Test: Scaffold a codex project, mutate one spec-kitty.<command>/SKILL.md so its bytes differ from both the manifest hash and the render, run the reconcile through the real upgrade/init entry point, and assert the file is regenerated to the canonical render with verify_installed_skills(...).ok and zero drift.
Acceptance Scenarios:
1. Given a manifest-owned command-skill projection whose on-disk bytes match neither the recorded content_hash nor the current render, When the install/upgrade/sync/init reconcile runs without explicit overwrite consent, Then the projection is regenerated to the canonical render, the manifest records that render's hash, and no consent_required disposition or "local edit was kept" message is produced. 2. Given a projection already in sync with the current render, When the reconcile runs, Then the file is left byte-unchanged and no needless rewrite is reported (idempotency / same-fixture negative control). 3. Given a drifted projection, When the reconcile runs twice, Then the second run reports no changes (stable fixed point).
User Story 2 - User-owned and third-party paths under .agents/skills stay untouched (Priority: P1)
A user who placed their own skill directory, or hand-authored a non-spec-kitty file, under .agents/skills/ expects it to be preserved even while manifest-owned projections regenerate.
Why this priority: Regenerating derived state must not become a licence to clobber content the installer does not own. This is the shared-root coexistence contract (installer NFR-002) and the mission's explicit non-goal ("never clobber non-manifest-owned paths").
Independent Test: Place a non-manifest file at a spec-kitty.* path and a third-party skill directory under .agents/skills/, run the reconcile, assert both are byte-identical afterwards.
Acceptance Scenarios:
1. Given a file at a spec-kitty.* path that is not in the command-skill manifest and whose bytes differ from the render, When the reconcile runs, Then it is preserved (never overwritten). 2. Given a third-party directory under .agents/skills/, When the reconcile runs, Then it is untouched.
User Story 3 - Docs and doctor state the generated/ignored policy (Priority: P2)
A user confused about whether .agents/skills is generated, tracked, or ignored wants the docs and doctor skills guidance to say plainly that it is generated/local-only, that it is regenerated by init/upgrade/sync, and what fresh-clone behaviour to expect.
Why this priority: Several of #702's acceptance criteria are documentation/clarity items. They close the "which output do I trust?" confusion but do not themselves change runtime behaviour.
Independent Test: Grep the affected docs for the stated policy; confirm no doc instructs a plain git add .agents/skills as normal project content.
Acceptance Scenarios:
1. Given the Codex/agent-skills documentation, When a user reads the generated-state policy, Then it states .agents/skills is generated/ignored and regenerated by init/upgrade/sync. 2. Given doctor skills output and the CHANGELOG, When a user hits drift, Then the supported idempotent-repair behaviour (automatic regeneration on upgrade/sync/init) is documented.
Edge Cases
- A projection whose bytes equal the recorded manifest hash but not the current render (a genuine version-upgrade re-render): already handled — it is re-rendered, not flagged; this mission must not regress that path.
- A
spec-kitty.*parent that is a symlink (owned package link) or a non-directory: the installer's existing unsafe-path / package-link handling is unchanged. - A drifted projection for an agent no longer configured: pruning/removal semantics are out of scope (the reported symptom is install-time drift of a configured agent).
Requirements (mandatory)
Functional Requirements
| ID | Title | User Story | Priority | Status | Delivery | No-op passable? |
|---|---|---|---|---|---|---|
| FR-001 | Regenerate drifted manifest-owned DERIVED command-skill projection | As an operator, I want a drifted .agents/skills/spec-kitty.<cmd>/SKILL.md regenerated to the canonical render on install/upgrade/sync/init so generated state matches the manifest. | High | Open | [build] | no — paired with the in-sync negative control (US1 scenario 2) on the same fixture |
| FR-002 | Preserve non-manifest / third-party paths under .agents/skills | As a user, I want my own skills and non-manifest files under .agents/skills left untouched while manifest-owned projections regenerate. | High | Open | [ratchet] | no — a non-manifest spec-kitty.* file stays byte-identical across the same reconcile that regenerates a manifest-owned one |
| FR-003 | Document the generated/ignored .agents/skills policy and idempotent repair | As a confused user, I want docs and doctor/CHANGELOG to state that .agents/skills is generated/ignored and regenerated by init/upgrade/sync. | Medium | Open | [folded] | yes — documentation only |
Non-Functional Requirements
| ID | Title | Requirement | Category | Priority | Status |
|---|---|---|---|---|---|
| NFR-001 | Idempotency against the manifest | After one install/upgrade/sync/init reconcile, verify_installed_skills(project) reports ok=True with drifted == [] for every configured command-skill agent; a second reconcile mutates 0 files. | Reliability | High | Open |
| NFR-002 | Shared-root coexistence | Across the regenerating reconcile, 0 manifest-foreign paths under .agents/skills/ are created, modified, or deleted (installer NFR-002 preserved). | Safety | High | Open |
| NFR-003 | Deterministic render | Regenerated bytes equal the canonical command_renderer output across repeated runs for codex/vibe/pi/letta. | Correctness | High | Open |
Constraints
| ID | Title | Constraint | Category | Priority | Status |
|---|---|---|---|---|---|
| C-001 | Scope held to the shared command-skill generator | Change is limited to the DERIVED .agents/skills/ command-skill installer path (codex/vibe/pi/letta). No change to tracked slash-command surfaces, the .claude/skills/* managed-file installer, or .claude/agents/.md profiles. | Technical | High | Open |
| C-002 | No suppressions; gates stay green | New/changed code passes ruff, ruff format --check, and mypy with zero issues; cyclomatic complexity ≤ 15; no # noqa / # type: ignore / Sonar suppression added to pass a gate. | Technical | High | Open |
| C-003 | State contract is the single authority | The regenerate decision is justified by, and consistent with, the shared_skills_projection surface (authority=DERIVED, git_class=IGNORED); a guard test pins that premise. | Technical | High | Open |
Key Entities
- Command-skill projection:
.agents/skills/spec-kitty.<command>/SKILL.md— DERIVED, git-ignored generated output; one per canonical command per shared-root agent. - Command-skills manifest:
.kittify/command-skills-manifest.json— tracked record of each projection's owning agents,content_hash, install time, and version. - Disposition: the installer's per-path verdict (
added/reused_shared/unchanged/preserve/consent_required);consent_required/preserveare the "unresolved" states the tool-surface provider surfaces as drift.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001:
spec-kitty upgrade --projecton a project whose Codex command-skill projection has drifted exits 0 and leavesdoctor skills --jsonwithok: trueandgaps: [](0 drift) — [build] · no-op passable: no - SC-002: An issue-pinned regression test driving the real upgrade/init reconcile is RED before the fix and GREEN after it, for at least codex plus one more shared-root agent — [ratchet] · no-op passable: no
- SC-003:
make test-fast, the skills/manifest/agent-config/doctor test suites,tests/agent/, ruff,ruff format --check, and mypy are green on the diff (modulo honestly-classified pre-existing baseline reds) — [ratchet] · no-op passable: no - SC-004: Documentation states the
.agents/skillsgenerated/ignored policy, the idempotent-regeneration repair behaviour, and fresh-clone expectations; no doc tells users togit add .agents/skillsas normal content — [folded] · no-op passable: yes