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:
probereports 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/--fixinvocation reaches the canonical state; a second invocation makes no change. - Honest failure: an unrepairable residual drift is surfaced as a non-zero/
failedoutcome, 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
| ID | Title | User Story | Priority | Status |
|---|---|---|---|---|
| FR-001 | Repair root-level context files by real parent | As 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. | High | Open |
| FR-002 | Refresh GEMINI.md on upgrade and --fix | As an operator, I want GEMINI.md's orientation block refreshed by both upgrade and doctor tool-surfaces --fix, matching the other orientation surfaces. | High | Open |
| FR-003 | Fix the llxprt latent twin | As 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. | High | Open |
| FR-004 | Never silent-skip a detected-stale writable surface | As an operator, I want a selected, detected-stale surface that repair cannot write reported as a failure, never dispositioned not_applicable with exit 0. | High | Open |
| FR-005 | Windows-aware directory-mode completion re-check | As 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. | High | Open |
| FR-006 | Upgrade converges in one invocation | As 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. | High | Open |
| FR-007 | No phantom dry-run repairs after convergence | As a Windows operator, I want upgrade --dry-run to report zero repairs once converged (no forever-re-planned chmod effects). | Medium | Open |
| FR-008 | Deterministic command-skills manifest content | As 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. | Medium | Open |
| FR-009 | Honest residual-drift reporting | As an operator, I want any residual unrepairable drift surfaced as a non-zero/failed outcome naming the file(s), never swallowed to exit 0. | High | Open |
Non-Functional Requirements
| ID | Title | Requirement | Category | Priority | Status |
|---|---|---|---|---|---|
| NFR-001 | Single-pass convergence | For 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. | Reliability | High | Open |
| NFR-002 | Detect/repair applicability parity | For 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). | Correctness | High | Open |
| NFR-003 | POSIX correctness preserved off-Windows | The 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). | Correctness | High | Open |
| NFR-004 | No new dependencies / no packs edits | The 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). | Maintainability | Medium | Open |
Constraints
| ID | Title | Constraint | Category | Priority | Status |
|---|---|---|---|---|---|
| C-001 | Fix generating code, never generated files | Repair 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). | Technical | High | Open |
| C-002 | Preserve the ADR writer architecture | Honor 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. | Technical | High | Open |
| C-003 | Windows-awareness must not weaken POSIX | The mode-comparison change is host-aware, not a blanket removal of mode checking; Linux/macOS keep full POSIX-mode correctness (NFR-003). | Technical | High | Open |
| C-004 | Out-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. | Technical | High | Open |
| C-005 | ATDD red-first, targeted tests | Every FR lands with a red-first test in the same change; Windows behavior is simulated by monkeypatching observed mode (POSIX-vacuity pattern), not skipped. | Process | High | Open |
Key Entities
- Orientation writer: a
session_presencewriter projecting an orientation/context file for one harness; carries the applicability predicate (can_write) that must match detection. - Surface repair service: the shared
SurfaceRepairServicedriving bothupgradeanddoctor tool-surfaces --fixthrough 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(andLLXPRT.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:
upgradeconverges 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-runreports zero repairs once converged under the Windows condition (no phantomchmodre-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/
failedoutcome 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) |