Mission Specification: Tool-surface projection honesty

Mission Branch: fix/tool-surface-projection-fidelity Created: 2026-09-20 Status: Draft Input: Milestone 11 (MVP launch) — GitHub issues #4782, #4776, #4777, #4134. Grounded by a code+architecture research squad; scope confirmed (core + Windows fold + #4134 ride-along).

Overview

spec-kitty upgrade and spec-kitty doctor tool-surfaces --fix bring an existing project's agent-facing files (orientation blocks, doctrine/command skills) up to date. Both are dishonest today: they report success or partial progress while leaving surfaces their own detector flagged as stale, or abort mid-run so the command must be re-run several times to converge.

Two distinct root-cause seams share one anti-pattern — repair reports success without converging:

1. Session-presence repair silently skips a detected-stale, writable surface (#4782). The repair path gates each orientation writer on can_write(); the gemini writer checks for a sibling command directory (.gemini/) even though its managed file GEMINI.md lives at the repository root. When .gemini/ is absent, gemini is dispositioned not_applicable, so --fix exits 0 having changed nothing — while detection still (correctly) reports the block stale. llxprt/LLXPRT.md is the identical latent victim. 2. Managed-skill completion re-check cannot converge in one run (#4776 + #4777 + #4134). After writing command-skill output, _recheck_command_completion asserts each destination's observed state equals the plan. Two things make that assertion unsatisfiable in a single pass: (a) it compares a directory's POSIX mode (0o755) on a freshly-created .agents/skills, which on Windows os.chmod cannot represent, so the re-check raises (#4776) or the chmod effect is re-planned forever (#4777); and (b) the command-skills manifest bakes a non-deterministic wall-clock installed_at, so a file effect's hash can differ between assessment and re-check (#4134).

This mission makes the repair path honest: repair every surface the detector flags, converge in a single invocation, and report residual drift as a failure rather than exiting 0.

Domain Language (canonical terms)

  • Tool surface: an agent-facing artefact projected from canonical sources — orientation/context files, doctrine skills, command skills, slash commands, native config. Generated by upgrade/regen; never hand-edited (CLAUDE.md "Template Source Location").
  • Detect vs repair: probe reports a surface's state (e.g. STATE_STALE); repair (assess → apply → re-check) brings it to the canonical state. The bug class is detect/repair asymmetry — repair's applicability or completion contract is narrower/stricter than detect's.
  • Root-level context file: an orientation file that lives at the repository root (GEMINI.md, LLXPRT.md, AGENTS.md), not inside a harness command directory. Its writability depends on the repo root existing, not on a sibling harness dir.
  • Completion re-check: _recheck_command_completion — the post-apply assertion that each written destination matches the plan. Must tolerate design-legitimate non-identity (a POSIX mode that the host OS cannot represent; a wall-clock timestamp) rather than aborting.
  • Converge / no-op: a single upgrade/--fix invocation reaches the canonical state; a second invocation makes no change.
  • Honest failure: an unrepairable residual drift is surfaced as a non-zero/failed outcome, never swallowed to exit 0.

User Scenarios & Testing (mandatory)

User Story 1 - Repair every detected-stale orientation surface (Priority: P1)

An operator whose GEMINI.md orientation block is stale runs spec-kitty doctor tool-surfaces --tool gemini --fix (or spec-kitty upgrade) and the block is actually refreshed — regardless of whether a .gemini/ command directory exists.

Why this priority: #4782 — the tool's own recommended repair is a silent no-op; the file has been stale across five upgrades on two machines. Directly undermines trust in --fix.

Independent Test: seed a project with a stale GEMINI.md and no .gemini/; run --fix; assert the block is rewritten to the current version and the run reports it repaired.

Acceptance Scenarios:

1. Given a stale GEMINI.md and no .gemini/ directory, When doctor tool-surfaces --tool gemini --fix runs, Then GEMINI.md's orientation block is refreshed to the current version and the surface is reported repaired (not skipped/not_applicable). 2. Given the same, When spec-kitty upgrade runs, Then GEMINI.md is refreshed alongside the other orientation surfaces. 3. Given a stale LLXPRT.md and no .llxprt/, When --fix runs, Then it is refreshed too (the latent twin is fixed, not just gemini). 4. Given a surface the detector reports stale that the repair genuinely cannot write, When --fix runs, Then it is reported as a failure, never a silent not_applicable exit-0. 5. Given an already-current GEMINI.md, When --fix runs, Then it is left unchanged (no spurious rewrite).


User Story 2 - Upgrade converges in a single invocation (Priority: P1)

An operator (or CI) runs spec-kitty upgrade once and it converges — doctrine and command skills are all projected — instead of aborting partway and demanding a second and third run. This holds on Windows.

Why this priority: #4776/#4777 — on Windows the first run aborts at the completion re-check and doctrine skills never apply; the reporter needed three invocations. Blocks the MVP Windows cold-start (#4725).

Independent Test: with the completion re-check observing a directory whose mode cannot equal 0o755 (Windows-simulated), run upgrade once; assert all skills project and the run exits 0; a second run is a clean no-op.

Acceptance Scenarios:

1. Given a project needing command + doctrine skill projection where the freshly-created shared skills directory reports a mode ≠ 0o755 (Windows behavior), When upgrade runs once, Then it converges (all skills applied), exits 0, and does not raise Completed command output changed. 2. Given a converged project, When upgrade runs again, Then it reports no changes (idempotent no-op). 3. Given the same Windows-mode condition, When upgrade --dry-run runs after convergence, Then it reports zero phantom repairs (closes #4777's re-planned-chmod symptom). 4. Given a re-check destination whose only difference from the plan is a host-unrepresentable POSIX mode, When the re-check runs, Then it treats the destination as satisfied (does not abort on a difference the OS cannot eliminate).


User Story 3 - Manifest content is deterministic (Priority: P2)

Re-assessing an already-installed command skill produces byte-stable manifest content, so the completion re-check does not fail on a wall-clock timestamp that changed between assessment and re-check.

Why this priority: #4134 — installed_at non-determinism can make the same re-check fail as a file-hash mismatch (a race), distinct from the Windows directory-mode cause but on the same function. Ride-along on the recheck seam.

Independent Test: install a command skill, re-assess it without changing inputs, assert the manifest entry's re-computed content is identical (no wall-clock churn drives a diff).

Acceptance Scenarios:

1. Given an installed command skill whose inputs are unchanged, When it is re-assessed, Then its manifest entry is byte-stable (a wall-clock installed_at does not cause a spurious content difference). 2. Given a large multi-skill projection written in one pass, When the completion re-check runs, Then sibling directory-mtime churn during the pass does not fail the re-check.

Edge Cases

  • Root-level context file whose harness dir is present (.gemini/ exists) → still refreshed (must not regress copilot/claude/gemini-with-dir, which work today).
  • A surface legitimately not selected (harness not in the configured agent set) → correctly not_applicable, distinct from a selected, stale, writable surface that must never be skipped.
  • Non-Windows host → the mode-aware re-check must not weaken POSIX correctness (a genuinely wrong mode on Linux is still a real drift to repair/report).
  • Partial prior run (command skills applied, doctrine skills not) → a subsequent run completes convergence and reports accurately.
  • Residual unrepairable drift → reported as failure with the offending file(s) named, not exit 0.

Requirements (mandatory)

Functional Requirements

IDTitleUser StoryPriorityStatus
FR-001Repair root-level context files by real parentAs an operator, I want the repair path to judge a root-level orientation file (GEMINI.md, LLXPRT.md) writable by its actual parent (the repo root), not a sibling harness command dir, so a detected-stale block is actually repaired.HighOpen
FR-002Refresh GEMINI.md on upgrade and --fixAs an operator, I want GEMINI.md's orientation block refreshed by both upgrade and doctor tool-surfaces --fix, matching the other orientation surfaces.HighOpen
FR-003Fix the llxprt latent twinAs an operator, I want LLXPRT.md repaired by the same mechanism, so the fix closes the class of root-level context files, not just gemini.HighOpen
FR-004Never silent-skip a detected-stale writable surfaceAs an operator, I want a selected, detected-stale surface that repair cannot write reported as a failure, never dispositioned not_applicable with exit 0.HighOpen
FR-005Windows-aware directory-mode completion re-checkAs a Windows operator, I want the directory-mode completion re-check to treat a directory whose only divergence is a host-unrepresentable POSIX mode as satisfied, so upgrade does not abort on the freshly-created .agents/skills. This covers BOTH host-unaware gates: managed_skills.py:179 (command re-check) AND installer.py:498 (_command_parent_receipts, doctrine apply) — the twin gate found during WP02 implementation; relaxing only the first leaves doctrine skills refusing on Windows.HighOpen
FR-006Upgrade converges in one invocationAs an operator, I want a single upgrade to project all command and doctrine skills and exit 0, with a second run a clean no-op.HighOpen
FR-007No phantom dry-run repairs after convergenceAs a Windows operator, I want upgrade --dry-run to report zero repairs once converged (no forever-re-planned chmod effects).MediumOpen
FR-008Deterministic command-skills manifest contentAs a maintainer, I want re-assessing an unchanged command skill to yield byte-stable manifest content, so a wall-clock installed_at does not drive a spurious completion-re-check diff.MediumOpen
FR-009Honest residual-drift reportingAs an operator, I want any residual unrepairable drift surfaced as a non-zero/failed outcome naming the file(s), never swallowed to exit 0.HighOpen

Non-Functional Requirements

IDTitleRequirementCategoryPriorityStatus
NFR-001Single-pass convergenceFor every affected surface, one upgrade/--fix invocation reaches the canonical state and an immediate second invocation makes zero changes — verified by test for both the orientation and skills seams.ReliabilityHighOpen
NFR-002Detect/repair applicability parityFor every supported, selected orientation surface, the set the repair path treats as applicable equals the set the detect path evaluates — no surface is detected-stale yet repair-skipped (verified by an enumeration test over all writers).CorrectnessHighOpen
NFR-003POSIX correctness preserved off-WindowsThe mode-aware re-check changes zero behavior on a POSIX host where os.chmod is faithful: a genuinely wrong directory mode is still detected and repaired/reported (verified by test).CorrectnessHighOpen
NFR-004No new dependencies / no packs editsThe fix is confined to Python source + tests; no new runtime dependency and no packs/ template-source edits (orientation is runtime-generated, so no regen-assets gate is triggered).MaintainabilityMediumOpen

Constraints

IDTitleConstraintCategoryPriorityStatus
C-001Fix generating code, never generated filesRepair the code that projects surfaces (session_presence/, tool_surface/, skills/), never hand-edit a generated GEMINI.md/.claude/ copy (CLAUDE.md Template Source Location; DIRECTIVE_044 red-line).TechnicalHighOpen
C-002Preserve the ADR writer architectureHonor the session-presence multi-harness ADR (2026-06-07-2): four pattern-writers, "we do not silently skip a supported harness". The fix removes a silent skip; it does not add per-writer parity patches.TechnicalHighOpen
C-003Windows-awareness must not weaken POSIXThe mode-comparison change is host-aware, not a blanket removal of mode checking; Linux/macOS keep full POSIX-mode correctness (NFR-003).TechnicalHighOpen
C-004Out-of-scope surfaces tracked, not folded#2527 (doctrine-skill manifest-hash drift + stray dist/) and the orientation-refresh split-brain (≥3 implementations, a DIRECTIVE_044 structural unification) are OUT of scope; file/track under a new umbrella epic, do not implement here.TechnicalHighOpen
C-005ATDD red-first, targeted testsEvery FR lands with a red-first test in the same change; Windows behavior is simulated by monkeypatching observed mode (POSIX-vacuity pattern), not skipped.ProcessHighOpen

Key Entities

  • Orientation writer: a session_presence writer projecting an orientation/context file for one harness; carries the applicability predicate (can_write) that must match detection.
  • Surface repair service: the shared SurfaceRepairService driving both upgrade and doctor tool-surfaces --fix through each provider's assess→apply→re-check.
  • Completion re-check: _recheck_command_completion — the post-apply assertion whose comparison must tolerate host-unrepresentable modes and non-deterministic timestamps.
  • Command-skills manifest: command-skills-manifest.json — must carry deterministic content for stable re-assessment.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: A stale GEMINI.md (and LLXPRT.md) with no harness command dir is refreshed by a single --fix/upgrade — 100% of the affected root-level context surfaces, where before the repair was a no-op.
  • SC-002: upgrade converges in one invocation under the Windows directory-mode condition (all command + doctrine skills applied, exit 0); a second run reports zero changes.
  • SC-003: upgrade --dry-run reports zero repairs once converged under the Windows condition (no phantom chmod re-plans).
  • SC-004: For every supported selected orientation surface, detect-applicable == repair-applicable (no detected-stale surface is repair-skipped) — enumeration test passes.
  • SC-005: A residual unrepairable drift produces a non-zero/failed outcome naming the file(s) in 100% of cases — never exit 0.
  • SC-006: On a POSIX host, a genuinely wrong directory mode is still detected and repaired/reported (no regression from the Windows-awareness change).

Out of Scope

  • #2527 — managed doctrine-skill manifest-hash drift and the stray untracked dist/spec-kitty-plugins/ output (adjacent theme, different surface/mechanism). Track under the umbrella epic.
  • Orientation-refresh split-brain — consolidating the ≥3 refresh implementations (SessionPresenceManager, RefreshOrientationBlockMigration, SessionPresenceProvider.repair) into one authority is a DIRECTIVE_044 structural refactor; note as follow-up, do not fold.
  • Any change to the surface content (the orientation body text) beyond refreshing it to the current canonical version.

Traceability

Requirement(s)Source issue
FR-001, FR-002, FR-004, NFR-002#4782
FR-003#4782 (llxprt latent twin)
FR-005, FR-006, NFR-001, NFR-003#4776
FR-007#4777
FR-008#4134
FR-009#4782 + #4776 (the shared honesty anti-pattern)
C-004#2527 + orientation-refresh split-brain (umbrella, deferred)