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

IDTitleUser StoryPriorityStatusDeliveryNo-op passable?
FR-001Regenerate drifted manifest-owned DERIVED command-skill projectionAs 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.HighOpen[build]no — paired with the in-sync negative control (US1 scenario 2) on the same fixture
FR-002Preserve non-manifest / third-party paths under .agents/skillsAs a user, I want my own skills and non-manifest files under .agents/skills left untouched while manifest-owned projections regenerate.HighOpen[ratchet]no — a non-manifest spec-kitty.* file stays byte-identical across the same reconcile that regenerates a manifest-owned one
FR-003Document the generated/ignored .agents/skills policy and idempotent repairAs a confused user, I want docs and doctor/CHANGELOG to state that .agents/skills is generated/ignored and regenerated by init/upgrade/sync.MediumOpen[folded]yes — documentation only

Non-Functional Requirements

IDTitleRequirementCategoryPriorityStatus
NFR-001Idempotency against the manifestAfter 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.ReliabilityHighOpen
NFR-002Shared-root coexistenceAcross the regenerating reconcile, 0 manifest-foreign paths under .agents/skills/ are created, modified, or deleted (installer NFR-002 preserved).SafetyHighOpen
NFR-003Deterministic renderRegenerated bytes equal the canonical command_renderer output across repeated runs for codex/vibe/pi/letta.CorrectnessHighOpen

Constraints

IDTitleConstraintCategoryPriorityStatus
C-001Scope held to the shared command-skill generatorChange 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.TechnicalHighOpen
C-002No suppressions; gates stay greenNew/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.TechnicalHighOpen
C-003State contract is the single authorityThe regenerate decision is justified by, and consistent with, the shared_skills_projection surface (authority=DERIVED, git_class=IGNORED); a guard test pins that premise.TechnicalHighOpen

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/preserve are the "unresolved" states the tool-surface provider surfaces as drift.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: spec-kitty upgrade --project on a project whose Codex command-skill projection has drifted exits 0 and leaves doctor skills --json with ok: true and gaps: [] (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/skills generated/ignored policy, the idempotent-regeneration repair behaviour, and fresh-clone expectations; no doc tells users to git add .agents/skills as normal content — [folded] · no-op passable: yes