Contracts

acceptance.md

Acceptance and Anti-Vacuity Contract

Audience: agentic-framework-core-team. Updated: 2026-09-06. Planned checks, not executed evidence. Instantiates Renata's G/B/P matrices.

Harness Boundary

Invoke the real source console entrypoint through a subprocess, resolving the absolute .venv/bin/spec-kitty before changing child HOME. Record source commit, module/distribution location and package version. No replacement Typer app, root-callback patch, mocked provider/installer or bootstrap suppression. Closed stdin, bounded timeout, capture complete stdout/stderr. Healthy output and expected semantic result must accompany purity; a startup crash is not pass.

Prepare every cell independently in a disposable Git repo/home. Canonical setup/manifest writers establish realistic fixtures before the measured snapshot; inject each deliberate defect afterward. For cold homes, prepare project assets using another setup home, then attach a genuinely untouched home. No init/profile/doctor call may prewarm the measured cold home.

Explicit child environment binds HOME, USERPROFILE, XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME, XDG_STATE_HOME, APPDATA, LOCALAPPDATA, SPEC_KITTY_HOME and temporary roots inside sandbox. Clear/rebind inherited asset/config/source overrides that could escape it. SaaS sync=0 set last. All symlink targets/sentinels remain in sandbox. No production network. PYTHONDONTWRITEBYTECODE=1 for source runs. Git observations use GIT_OPTIONAL_LOCKS=0. Keep CI=1 normal cells plus non-CI/TTY cells below; CI suppression must not be the reason preview is pure.

Independent lstat oracle records every node, normalized root/path, kind, SHA-256 bytes, exact readlink target, permission bits and mtime_ns. Include ignored/untracked files, empty dirs, dangling links, runtime/cache/locks, manifests, custom configs and third-party sentinels. No broad .kittify/.git/cache exclusion. Record HEAD/refs/index separately from asset effect equality. Read access times excluded. Across separate copies, compare content/type/mode/ target and normalized effects, not raw mtimes; within each preview/idempotent apply cell, mtimes must remain unchanged too.

Oracle controls must detect, independently: ignored-file edit; empty-directory creation/removal; dangling-link deletion; retarget; chmod; same-bytes rewrite with changed mtime. A version which deliberately omits each observation must fail its control. Production effect-building helpers cannot implement the oracle.

Snapshot equality proves persistent purity, not transient write/delete absence. In addition, instrument the real process's assessment boundary using a recording/ denying audit hook or platform filesystem tracer covering create/open-for-write, mkdir, unlink/rename, chmod, utime and link operations. Do not stub the owners. Install the hook before CLI imports in a test-only entry wrapper calling the real main; retain ordinary unwrapped subprocess snapshots as the primary witness. Record any unsupported syscall coverage explicitly, and test attempted write-then-delete as a negative control. Do not claim zero syscalls from snapshots.

A: Global Purity Matrix

IDHome state
G0Runtime and all global tool roots absent
G1Real populated runtime, skills and commands all stale
G2Only runtime stale marker and recognizable managed bytes
G3Only global skills stale marker/content
G4Only global commands stale marker/content
G5Fully healthy/current, no cleanup candidates
G6Current markers but required command missing or stale file marker
G7Current markers plus a proven-owned retired skill cleanup candidate

Run all 8 against a same-version healthy project, in human --dry-run --verbose and legacy --dry-run --json, each with/without --project; all --no-worktrees. 32 independent public cells. Run --plan-json on G0/G1/G5/G6/G7 (5 more); legacy strict-schema checks remain in every legacy cell. Require G0/G1 human and full plan to describe real pending global preparation, no startup mutation. Legacy summary must disclose pending work and point to the full plan.

B: Target Matrix

Healthy G5, current supported schema, explicit metadata version 3.2.7rc1. These numbers are historical test fixtures, not release decisions.

RequestedRelation / target validity
3.2.6lower / reject
3.2.7rc0lower / reject
3.2.7rc1equal / valid
3.2.7rc2higher / valid
3.2.7higher / valid
3.2.8higher / valid, no invented future migrations

All six x human/legacy JSON x with/without --project, --no-worktrees: 24 cells. Add one full-plan cell per row and malformed targets empty string, not-a-version and control-character input across human/legacy/full. Assert explicit bounded errors and no traceback/mutation. Add omitted target, unknown current, malformed known metadata, and current schema > MAX_SUPPORTED. Pair lower and higher targets with stale, too-new and corrupt schema to prove independent reasons and precedence. Do not exhaustively multiply unrelated axes.

For legacy lower-target compatible project: decision BLOCK_INCOMPATIBLE_FLAGS, case none, payload exit_code 2, current/target reason, project.state compatible, process 0 for --dry-run. Human exits 1; full exits 2. Too-new legacy dry-run retains process 5. No target-validity test may assert migrations=[] means valid or higher implies ALLOW in the presence of independent schema blockers.

C: Same-Version Plan/Apply Matrix

G5 default, current version/schema, no pending migrations. Realistic configured claude/codex/cursor baseline plus isolated native-config/bundle cases where applicable; no forced activation just to manufacture repair counts.

IDFixture / witness
P0Repaired baseline, first and second apply both no persistent effects
P1Missing managed command files, nonempty creation
P2Missing managed skills, nonempty creation
P3Missing profile projections, nonempty creation
P4Missing command/profile manifests, retained generated files; prove safe adoption or preserve ambiguity
P5Truncated/stale manifests plus stale managed orientation/config, nonempty appropriate updates
P6Combined commands/skills/profiles/manifests, all applicable families nonempty
P7P6 plus missing mission_type_activations, provisioning represented
P8Missing + drift + unknown custom command/skill + sandbox symlink sentinel

Repeat P6 with G0/G1 (11 variants total). For each, on independent equivalents: human preview, legacy JSON preview, full-plan preview, actual upgrade --yes --no-worktrees; then on the resulting state repeat the three previews and second actual apply. 8 processes per variant, 88 total excluding fixture setup. Legacy remains strict; full plan supplies exact path effects. Human output must disclose the same operations without parsing an internal API. This extends Renata's six-process matrix for the chosen separate plan surface.

Derive actual net persistent delta independently, normalized per owner-operations.md. Compare effect sets exactly on deterministic successful fixtures: roots, paths, action and final type/mode; every supporting manifest, directory, backup and pruning path included. Nonempty P1/P2/P3/P6 witnesses prevent vacuous equality. Do not compare logical surface counts with file counts. P8 permits known independent repairs while drift stays unchanged/reported with existing noninteractive failure; second apply must still have zero new churn.

After hashes are per-assessment desired bytes, not promises of identical clocks across separate processes. For cross-copy rendered manifests, normalize only documented newly assigned installed_at/updated_at/last_upgraded_at timestamps where the existing owner schema uses them; retain exact field paths and raw copies in evidence. The sole additional creation-time allowance is root /created_at of .kittify/skills-manifest.json newly created from an absent manifest in both independent baselines; retain absence proof and raw hashes. Do not normalize any pre-existing manifest creation time or mission-meta created_at. Do not normalize entire manifests, identities, source hashes, owner sets or substantive content. Within a single process, applied bytes must equal its prepared bytes; unchanged manifests cannot refresh dates.

Focused Boundary Cells

provisions. Retain existing provisioning/migration selector regression cases.

public preview is complete/write-free and apply preserves comments/unrelated sections with bytes matching its preparation. Explicit empty is unchanged in both layouts; dangling pointer is an honest blocked assessment. Keep the charter layer gate rejecting imports of specify_cli operations.

T1 and applied at T2 retains exact prepared bytes/hashes and backup paths. Equivalent independent public preview/apply fixtures at different times select identical persistent backup paths/actions, including existing candidate and suffix collisions. Preserve every prior backup, verify exclusive creation race refusal, and repeat apply for no churn. Clock injection at an owner seam supplements rather than replaces public subprocess/snapshot witnesses. A writer that resamples manifest time and an allocator that uses current time are negative controls; both must fail. Never normalize persistent paths.

reach the real managed-skills owner with .kittify/skills-manifest.json absent in both baselines. Only its newly allocated root /created_at receives the added normalization above. Changed pre-existing manifest /created_at and changed mission-meta created_at must each fail negative controls. Preserve raw hashes, exact per-invocation prepared bytes, preview mtimes and unchanged second-apply mtimes; no per-invocation normalization.

blocked/incomplete not ALLOW/empty, no all-agent fallback. A truly absent manifest is not treated as malformed; ownership of retained files is separate.

refcounts. Disabled agents/Amazon Q profile exception not auto-repaired.

preserved; stale full-agent bundle reports manifests and all changed members.

applicable staged bundle fixture covers output manifest and member files.

adoption hazard; unknown spec-kitty.custom symlink is never deleted.

symlink replacement: owner batch aborts before writes and reports conflict. A focused owner integration seam is appropriate for this race; do not replace the ordinary public subprocess acceptance with a mocked race test.

attempt or cache/show-time/preference persistence in preview.

--cli conflicts, preview plus hidden agent-choice: selected schema, purity and exit rules in upgrade-cli.md. Verify ordinary help/version/next/upgrade apply still work and are not accidentally made preview-only.

each of --agent-choice (valid choice/latest pair), --agent-check and --agent-latest alone; explicit --cli --json plus hidden flags; no-project ordinary guidance plus hidden flags. Require one pinned legacy conflict payload and process 2, complete unchanged snapshots and no transient writes. Repeat a representative conflict with --plan-json for full-envelope precedence. Standalone hidden check and valid choice/latest operations are positive controls retaining their existing outputs/exits and legitimate preference behavior; run those only in disposable homes. No full Cartesian flag expansion needed.

payload, BLOCK_CLI_UPGRADE, case project_too_new_for_cli, semantic/process 5, no bootstrap or other writes. Parse entire stdout and validate the schema. Pair with --plan-json too-new full-envelope/code-5 control; retain the distinct standalone invalid-target actual-outcome fixture. These are public source subprocess cells, not tests of the guard helper alone.

in that root; without it planned metadata/stamp work and actual outcome agree. Add existing fatal/skipped worktree behavior and dirty baseline/manual-review auto-commit tests, without multiplying all G/P cells across worktrees.

snapshots and verdicts untouched absent separate consent. Retain existing explicit-positive-consent test proving repair remains reachable.

  • Explicit empty mission_type_activations: [] remains untouched; missing key
  • Instantiate P7 for both legacy config and a commented pointer-based charter;
  • Clock cells: an ownership-proven, consent-permitted replacement assessed at
  • Missing-manifest clock cell: independent public preview at T1 and apply at T2
  • Truncated JSON/malformed manifest, corrupt agent config, missing package source:
  • Shared codex/vibe root and per-agent owners: one physical write, retained
  • Profile deactivation with an owned prune candidate; drifted prune candidate
  • Native orientation/settings/hook mixed file: preserve unowned bytes/keys;
  • Custom canonical-path content plus another missing command reproduces manifest
  • Change destination or manifest/source between assess/apply, including parent
  • Warm/cold non-CI TTY spot checks with cacheable version data: no network
  • Reordered options, --target=value, redundant --plan-json --json --dry-run,
  • Focused cold-home public hidden-intent cells: --project --json combined with
  • Default actual upgrade --json on too-new schema: full pinned legacy planner
  • One same-version worktree fixture: with --no-worktrees no changes/discovery
  • Blocked mission fixture: preview and upgrade --yes leave identity, events,

Red, Green and Installed Witness

Before implementation, witness RED through unchanged public commands for G0/G1 purity, P6 human/legacy omission versus real apply, lower-target legacy JSON, and original full corpus audit. A --plan-json unknown-option/import failure cannot serve as #3901 red evidence; it tests a new API, not the old defect. Each owner bugfix adds red behavior at its existing seam plus public blast radius. Commit the appropriate failing test before implementation (parent's workflow).

Final source suite runs all matrices. One independently installed wheel then replays G0/G1/G5 previews, lower/equal/higher/malformed legacy/full target cells, P6 plan/apply/repeat and separate-consent control. Resolve its executable absolutely and record wheel SHA/distribution/module provenance. Clear PYTHONPATH, template/source overrides injected by conftest and any editable-install path. Run from disposable projects outside source checkout; no source tree on sys.path. Use existing isolated build/test dependencies; no production dependency change or global tool install. Wheel build/install is future verification, not claimed executed by this plan.

Mutation controls: restored unconditional bootstrap, omitted manifest effect, downgrade ALLOW at process 0, omitted ignored-file observation, forced overwrite of user sentinel, and transient write/delete all must fail their relevant checks. Mutate only disposable test/source copies, never the shared checkout. Architecture gate must cover concrete entry/owner floor and self-mutation; green empty allowlists or zero discovered owners are failures.

Gates and Reporting

Owners run changed tests + full affected subsystem tests + make test-fast. Use direct warm .venv/bin/pytest, ruff and mypy with sync=0; no defensive uv sync. Run changed-module lint/types and relevant layer/import/dead-code gates; full tests/architectural for new architecture/shared-test changes under repo policy. No broad make test-full in per-WP runs. Record commands, counts, red reason, green result, commit/executable/wheel identity and snapshot hashes. Existing failures are classified and reported under repository policy, never retried to green or masked with skip/xfail. No tests or architecture/performance gates have been executed in this planning turn.

Measure cold/warm planning performance against <2s typical-project budget separately from expensive fixture creation/wheel build. Record baseline and final p50/p95; use recorded wall time, not a fragile universal timing assertion. Windows/macOS/Linux path/mode behavior needs evidence; capability-specific symlink restrictions must be reported and covered on a capable platform, not silently waived across all platforms.

Parent-Owned Final Gate Table

These are consolidation prerequisites, not additional per-WP broad suites or an authorization for this author to run tests. The runtime review CLI does not execute these gates; its PASS cannot substitute for them. Evidence sources: readiness/provenance, five-case current-source baseline and its preserved baseline-five-20260906-133810/ directory. CORE is this checkout; E2E is sibling spec-kitty-end-to-end-testing, origin [email protected]:spec-kitty/EXPERIMENTAL-spec-kitty-end-to-end-testing.git.

Gate / parent ownerRequired selection and pass evidencePrerequisites / fail-closed disposition
Full CORE contracts.venv/bin/pytest -c pytest.ini tests/contract/ -v -ra -p no:cacheprovider; full collected selection, packaging/envelope/consumer witnessesEffective fixture policy, isolated build/install and required dependencies established; missing imports or relevant importorskip are not a pass
Full CORE architecture.venv/bin/pytest -c pytest.ini tests/architectural/ -v -ra -p no:cacheprovider; full suite including anti-vacuity/mutation evidenceLiteral CI=true, reachable origin/main merge-base and efc0003b563d4a447e29bbfc726bf97c5179a209, formatter/pytestarch prerequisites; #3911 exact-operation remediation independently reviewed, no baseline shift/skip
Current cross-repo scenariosE2E .venv/bin/pytest -c pyproject.toml scenarios/ -v -ra -p no:cacheprovider; all current cases plus the five identities below executedCorrect E2E interpreter/config/import path and explicit CORE bindings; baseline two failures resolved/tracked under owner policy; inner drift assertion plus unmutated green control mandatory
Terminal issue matrix / parent mission reviewer#3900-3903 each has fixed, verified-already-fixed or explicitly accepted deferred-with-followup disposition, linked red/green/artifact and independent review evidencein-mission/pending is not terminal; no deferral silently weakens approved FRs. Gate failures and external followups have distinct explicit dispositions

Use SPEC_KITTY_ENABLE_SAAS_SYNC=0, CI=true, PYTEST_ADDOPTS cleared and PYTHONDONTWRITEBYTECODE=1 at launch; these settings alone do NOT satisfy the effective environment prerequisite. CORE tests/conftest.py:457-460 sets sync=1 in an autouse fixture. Parent must establish and record an approved harness policy preserving the requested sync-off execution boundary without disabling conftest wholesale or deselecting tests. This is an environment hazard, not evidence of live SaaS/network activity. No recursive lock/firewall was proven by the baseline's outer subprocess observer.

Parent has authorized isolated package installation; readiness's former no-install blocker is superseded, not the isolation/provenance requirements. Allow necessary package download/install only in disposable envs/caches under that authorization; do not call those runs network-free. No live credentials, hosted endpoints or SaaS actions. Record interpreter/pytest/dependency versions, package sources and warnings. Baseline nested Python 3.13.9/pytest 9.1.1 differed from outer E2E 3.13.1/8.4.2: resolve/record reproducibility, not a false root-cause waiver for the missing diary module. No production dependency changes.

Bind SPEC_KITTY_REPO and SK_E2E_SPEC_KITTY_REPO to the same absolute CORE; SK_E2E_SPEC_KITTY_BIN/PYTHON to its direct warm binaries. Ensure E2E package importability and its declared pytest range; avoid implicit uv/PATH fallback. Use disposable HOME/USERPROFILE/XDG/APPDATA/LOCALAPPDATA/runtime/temp roots, no host credentials, and preserve fixture evidence. Run serial first; any parent-authorized xdist CORE run uses --dist loadfile. Keep E2E configured marker exclusions; clearing inherited PYTEST_ADDOPTS is not removing config.

Five-Case Floor and Known Red

At CORE 6ada9613b8b8713809da4089bda11d9d35065a77 and E2E 1bc2a9b535a66ba27f2707e2344fc4284e896a26, Renata observed exactly 2 failed, 3 passed, 0 skipped, 0 deselected; exit 1, 329.07s. Paths below are relative to E2E scenarios/:

Exact node identityBaseline / required followup
dependent_wp_planning_lane.py::test_dependent_wp_planning_lane_lifecycle_smokeFAIL, child-0066: WP04 planning-artifact claim cannot establish approved lane-c ancestry. Tracked in CORE #3912. Parent routes actual planning-workspace/self-heal semantics to existing core owner; no guard bypass or manual fixture merge
uninitialized_repo_fail_loud.py::test_uninitialized_repo_fails_loud[specify]PASS, child-0074; rerun and retain real structured error witness
uninitialized_repo_fail_loud.py::test_uninitialized_repo_fails_loud[plan]PASS, child-0082; rerun and retain real structured error witness
uninitialized_repo_fail_loud.py::test_uninitialized_repo_fails_loud[tasks]PASS, child-0090; rerun and retain real structured error witness
contract_drift_caught.py::test_contract_drift_caughtFAIL, child-0096: fake events package missing spec_kitty_events.diary causes collection error, not intended envelope assertion. Tracked in E2E #411. Parent schedules existing harness owner; require realistic package surface, identified executed intended assertion and unmutated green control

Track both baseline failures with source SHA, exact traceback, owner and linked followup before any accepted pre-existing-failure disposition under the charter. They are product/harness defects, not environmental skips. No unconditional waiver and no duplicate harness-repair IC/WP here; parent controls separate authorized repair and rerun. Passing only the three uninitialized cases or an outer nonzero drift subprocess does not complete this floor. A tracked red remains red until explicit authorized disposition; never label it passed.

Retired SaaS Scenario and Evidence Closure

scenarios/saas_sync_enabled.py is RETIRED BY SOURCE, not skipped or passed. E2E deletion e59564bd8b82f7912b8db67087712ec84fc47cf8 and CORE accepted ADR docs/adr/3.x/2026-09-06-1-convergence-retirement-and-client-repo-inversion.md establish transport retirement; non-sync doctor coverage moved to tests/test_agent_status_doctor_e2e.py. Parent records the stale skill/older ADR selection mismatch with this provenance. Do not resurrect transport, enable hosted traffic, or substitute unexecuted Docker successor lanes. This is not a blanket Gate 3 exception or waiver of the surviving five cases.

For every final gate retain exact argv/cwd, source and wheel SHAs/dirty scope, effective config/environment, runner/dependencies, start/end/exit, total collected/executed/deselected/skipped with reasons, stdout/stderr and JUnit. Preserve nested drift output and actual assertion identity, not just outer exit. Parent adjudicates each relevant skip/failure independently and obtains reviewer signoff; no summary green can conceal missing coverage. Gate execution and terminal matrix evidence remain pending, not claimed by this planning document.

Additional parent gate-state evidence: golden-arity recurrence is recorded on existing CORE #3458. Parent reports narrow Op 01M1V8WKCPN7KZT629FP57NA2X fix: 33 passed, Ruff/mypy clean, with independent golden review APPROVE. Parent reports guard and golden fixes integrated at c3657a86a; integrated tests and original next are running. These parent-supplied results are not independently rerun here or full-gate approval. Retain their review/disposition in final evidence; do not create another architecture-source repair lane in this mission.

corpus-recovery.md

Corpus Recovery Contract

Audience: agentic-framework-core-team. Updated: 2026-09-06. Normative for FR-007-010 and NFR-004. This is a future execution plan, not authorization for blanket repair or a claim data has already changed.

Provenance and Exact Recovery Set

Original cyclic mission: reject-cyclic-lane-graphs-01M0QCK4. Original identity: 01M0QCK4D9D65AVNC15HKWAQZ7. Source commit: 3442ca1afc20b1b83b27a7bc64fd7014050b12a1. Convergence merge: 2554bd13adc289d3457681308645fe52619bca0e. The source tree at that mission path matches convergence's first public parent; path-scoped git diff was empty during planning. Source has 32 files; current HEAD retains only contracts/lane-dependency-cycle.schema.json, blob 26cb3b8bafde72894f0d1ec0a9c1701cd511f497, unchanged from source.

Restore these 31 missing paths, relative to that historical mission directory:

.kittify/dossiers/reject-cyclic-lane-graphs-01M0QCK4/snapshot-latest.json
acceptance-matrix.json
adversarial-review.md
analysis-report.md
checklists/requirements.md
data-model.md
decisions/DM-01M0QCNTD5CM0SE0HKQ79C9NF6.md
decisions/DM-01M0QDJWKXD5JHSVHV0NWSJDWM.md
decisions/DM-01M0QEAKZVM8QAVZPF9AE6D1N8.md
decisions/index.json
issue-matrix.json
lanes.json
meta.json
mission-review.md
plan.md
pr-summary.md
quickstart.md
research.md
retrospective.yaml
spec.md
status.events.jsonl
status.json
tasks.md
tasks/.gitkeep
tasks/README.md
tasks/WP01-authoritative-domain-cycle-gate.md
tasks/WP01-authoritative-domain-cycle-gate/review-cycle-1.md
tasks/WP01-authoritative-domain-cycle-gate/review-cycle-2.md
tasks/WP01-authoritative-domain-cycle-gate/review-feedback-1.md
tasks/WP02-finalization-diagnostics-and-persistence.md
tasks/WP03-determinism-performance-and-regression.md

Before restore, recheck source object availability and every current destination. A newly present divergent file is a conflict, not overwrite permission. Recover exact Git blob bytes and modes, including the zero-byte .gitkeep and nested dossier. Use the parent-authorized bounded Git/blob restoration workflow in the IC-08 isolated workspace; do not reset the entire directory or checkout. No metadata-only fix, new ULID, rewritten acceptance record or regenerated raw events. Do not rewrite historical vocabulary or author attribution.

Verify all 31 restored blobs against source before any materialization. Original created_at 2026-08-23T13:22:37.098012+00:00 and accepted_at 2026-08-23T17:12:57.441258+00:00 remain; raw event history has 71 rows in the verified source. Keep the surviving schema path so tests/specify_cli/cli/commands/agent/test_finalize_lane_dependency_cycle.py continues consuming it. A later derived snapshot correction is a distinct canonical replay step with before/after evidence; preserve the historical snapshot blob reference even if current replay differs.

R2-T1 Documentation Relocation

Classify kitty-specs/R2-T1-local-legacy-removal/ as a documentation bundle, not an identity-less mission. Move, byte-for-byte:

Old path under kitty-specs/R2-T1-local-legacy-removalNew path
deletion-manifest.mddocs/archive/program-evidence/R2-T1-local-legacy-removal/deletion-manifest.md
commit-history-notes.mddocs/archive/program-evidence/R2-T1-local-legacy-removal/commit-history-notes.md

The new archive directory does not yet exist; creating it is intentional repository-data work, outside immediate mission scanning. Retain exact document bytes and Git history; do not mint metadata, create a placeholder under kitty-specs or leave an alias directory that the audit still scans.

Provenance: deletion manifest introduced at 0be7d692adcc71503ad1c3b0ad6b0d16accdef62; attribution note at 557a55946d9a23f0c4f7bf4311eb02bb8d3d04cb. Record old/new SHA-256 and modes plus rename provenance. Scan tracked references across the repo, not just docs; update active navigation/test/code references through their file owners. Historical mission/issue text quoting the old path remains evidence, with an adjacent new archive index mapping old to new paths, not an edit of immutable historical records. Inspect relative links in moved documents; preserve content and supply needed context/mapping in the archive index if relocation changes their interpretation. No invented classification CLI.

Two Scoped Snapshot Repairs

Separately Adjudicated Restored-Mission Replay

Parent disposition, 2026-09-06: exact restoration exposed the anticipated cyclic snapshot drift. Independent Debugger Debbie reproduced both pins and traced every changed field to unchanged historical metadata/annotations. Authorize one additional derived replay, only kitty-specs/reject-cyclic-lane-graphs-01M0QCK4/status.json. This supersedes WP11/WP12 two-snapshot/eight-verdict total counts: two original repairs plus one separately recorded restored-mission replay, preserving eleven complete standing review_result objects (five plus three plus three).

  • Historical blob: b1925001e8a3bed8d95b06e1c7c166a45d3e4db0.
  • Historical SHA-256: fd53f81b4d658491d9035c5cedad769055639d717ac7117e3afb1a7294ccd640.
  • Reviewed canonical SHA-256: b20f7ea141534ceccc7a761fcbf4df9ae8a46eb337a20e4da371da4b37ed15f8.
  • Unchanged event SHA-256: 1502470338a65a478c8d04424b642552317eb95d1d96c526a8c2e444df456099.
  • Unchanged metadata SHA-256: 287f11b13d075c89fea910db940c668ef697ba072578958ceab765cf9693f203.

Restore and replay remain separate operations. Prove all 31 restored blobs and retained schema match historical input before replay; afterward only this status.json differs, with historical blob proof retained. Verify the supported CLI partition, then use agent status materialize --mission reject-cyclic-lane-graphs-01M0QCK4 --json. Require exact reviewed output and current read-only canonical replay; unchanged raw files/IDs/three approvals, done lanes/force counts/transition markers; and second-pass byte/mode/mtime idempotence. Shell projections change to existing winning annotations; do not claim those derived values remain unchanged.

WP12 independently binds this operation and pins, rejects forged receipt/output, altered history/verdict/identity, extra paths and post-landing mutations. No receipt-only authority, directory-wide exemption, raw-event repair or fourth snapshot admission. This disposition is not executed repair, WP approval or a waiver of the full zero-blocker audit. Independent external evidence: cyclic-replay-adjudication.md beside this checkout.

Original evidence (recheck before mutation; divergence requires adjudication):

MissionEvent SHA-256Original verdict/done lanes
doctrine-drg-silent-drop-boundary-01M0PE7E518d572629a39d026341d0cc26f47cc1a87c5f3323e21c5b8759cd0a02477b205
symbolkey-source-module-01M0B0SF7e0326d9d1a9af04f3d1ce353e0b97caba765aab298de01074df4b889a455aaf3

Capture current meta, snapshots, raw events, per-WP review_result objects, lanes, transition IDs, force provenance and annotated metadata before repair. Resolve each mission's real status partition through the supported command context in the IC-08 isolated checkout; verify the command will write the intended mission artifact, not an unrelated active mission or the parent's coord.

Then use the verified narrow command:

env SPEC_KITTY_ENABLE_SAAS_SYNC=0 .venv/bin/spec-kitty agent status materialize --mission doctrine-drg-silent-drop-boundary-01M0PE7E --json
env SPEC_KITTY_ENABLE_SAAS_SYNC=0 .venv/bin/spec-kitty agent status materialize --mission symbolkey-source-module-01M0B0SF --json

Expected: canonical annotation-aware replay adds/corrects metadata; doctrine mission_type becomes software-dev. All eight done lanes and complete review_result objects remain identical, including reference/reviewer/verdict. Event bytes/hashes, immutable IDs and raw annotation evidence remain unchanged. Do not replace absent historical model/provider values with invented data. Materialize twice; second pass changes nothing (bytes and mtimes).

The command may use resolver/lock infrastructure; inspect its actual output paths before accepting the diff. A misplaced output is a blocker to diagnose, not a reason to hand-write snapshots or edit coord. Avoid both global and mission-scoped doctor --fix, which can canonicalize raw events/metadata beyond this task. No reducer/audit source change is expected.

Final Evidence

The portable recovery receipt lives at docs/archive/program-evidence/upgrade-preview-mission-health-01M1V6E1/recovery-receipt.json. WP11 produces it with the confined corpus/document changes; WP12 owns tests/upgrade/test_mission_corpus_recovery.py and independently validates the receipt and original/corrupted counterfactuals. Neither the receipt producer nor the preservation-gate implementer may approve its own evidence exemption. WP11 still demonstrates real CLI failure before recovery and exact provenance, no-churn and full-audit success before its independent data review.

Run the real full-corpus gate in the repaired isolated checkout:

env SPEC_KITTY_ENABLE_SAAS_SYNC=0 .venv/bin/spec-kitty doctor mission-state --audit --fail-on teamspace-blocker --json

Require exit 0 and zero TeamSpace blockers, not merely four removed names. Record complete output or content hash plus exact filtered evidence, corpus count, executable/commit provenance, recovery blob manifest, rename hashes, snapshot diffs, eight unchanged review objects and second-run idempotence. Historical total 422/424 is not a fixed acceptance count.

If restoring the cyclic history exposes a derived-snapshot drift, preserve its raw recovered files and perform a separately recorded canonical scoped replay; do not alter raw history to match a stale snapshot. Any other newly exposed blocker needs explicit diagnosis/disposition, never weakened audit thresholds. Unrelated warning/info counts remain observed, not blanket remediation scope.

Retain negative upgrade --yes consent test and existing positive explicit mission-repair control. #3903 is committed-data repair; it does not widen upgrade consent or fold mission repairs into generated churn commits.

owner-operations.md

Owner Assessment and Application Contract

Audience: agentic-framework-core-team. Updated: 2026-09-06. Normative implementation contract for FR-001-004, C-001 and preservation.

Small In-Process Seam

Introduce immutable stdlib-only values in tool_surface/operations.py; no imports of registries, providers, runtime, upgrade, CLI or filesystem writers. An owner exposes this structural contract (names may be idiomatically integrated into its existing module; semantics are fixed):

assess(inputs, provider_owned_statuses) -> OwnerAssessment
apply(OwnerAssessment, explicit_consent) -> OwnerApplyResult

OwnerAssessment:
  owner_key
  effects: tuple[PhysicalEffect, ...]
  dispositions: tuple[Disposition, ...]
  diagnostics: tuple[Diagnostic, ...]
  complete: bool
  inputs_fingerprint: immutable owner input observations
  prepared: opaque immutable owner payload (rendered bytes/manifest changes)

This is not a generic writer callback framework. Existing owners retain their format-aware write/merge/atomic-save functions. They extract one decision/render/ compare path that both assess and apply use. Prepared data is never serialized as an executable token. No generic snapshot-overlay filesystem or rollback API.

SurfacePlanBuilder remains the configured tool/definition/provider inventory. Status collection preserves provider instance/hash/source/manifest context. SurfaceRepairService dispatches owner assessments and application, rather than reconstructing instances from findings. New assessment protocol is separate from the reporting protocol so unrelated consumers do not acquire a new mutation API accidentally; built-in upgrade providers must all implement it. Missing support on a selected required provider is an explicit incomplete assessment, never silently skipped. Registry is still the provider authority.

Effect Semantics

Wire schema is upgrade-plan.schema.json. Every effect has one executable owner, all affected logical owners/surface IDs, a root ID and normalized relative path. Ownership reference is a manifest entry or an exact existing managed-path contract; it cannot be a filename prefix. Paths from separate root aliases that name the same physical destination are deduplicated without following hostile links. Resolve configured roots once. Contradictory desired content for one path is an owner conflict, not last-writer-wins.

ActionObservable postcondition and independent snapshot normalization
createAbsent -> node present with declared final kind/mode; includes file bytes or link target where applicable
updateExisting regular file -> new bytes, with final mode if also changed
deleteExisting owned node -> absent; directory delete requires enumerated owned descendants and empty-directory check
replaceNode kind changes, notably managed link -> file copy; never follow link target
retargetExisting owned symlink -> different literal target (only where existing delivery policy permits links)
chmodSame node kind/content/target -> different permission bits

Existing project skill delivery stays copies, not newly introduced symlinks. File creation with its final mode is one create, not an extra chmod. For a same-type file changing bytes and mode, update captures both. The oracle emits this same independently specified net-delta normalization, not production effect helpers. All added/removed directories are represented. Parent mtimes changed incidentally by child creation are observed but not separate apply operations; preview must leave them unchanged.

Only effects correspond to automatic writes in the assessed consent mode. Unchanged/preserve/not_applicable/consent_required are dispositions, not effects. A preview does not prompt. A real interactive drift approval triggers a fresh owner assessment of that exact managed path; unknown custom paths cannot be approved into ownership through this flow. --yes leaves drift dispositions.

Include persistent supporting paths: metadata, config, all manifests/refcounts, version stamps, owned prune targets, backup files, copies, mode changes, and staged plugin outputs only when selected policy actually repairs them. Include persistent apply lock-file creation as infrastructure effects; transient temporary/lock artifacts are reported separately as owner execution artifacts, with concrete directory and bounded owner naming rule. They are permitted on apply only, never in preview. No hidden persistent writes may be excluded as "cache". No byte-level plan for Git's implementation internals is promised: commit_policy describes existing churn commits, and Git state is verified separately. Mission-state repair is never an effect in this plan.

Owner Coverage and Dependencies

Clock and Retained Backup Allocation

Persistent backup names must be state-derived, not wall-clock-derived. In the managed-skill owner, use the existing backup parent with a versioned state- name containing SHA-256 of a canonical, sorted encoding of replacement-relative paths, observed kind/content hash/literal link target/mode and desired stable replacement content/kind/mode. Do not include absolute fixture root, mtime, clock or new manifest timestamps in that identity. Retain the original relative layout inside the backup directory. This is a local concrete owner rule, not a shared allocator service; it applies only to ownership-proven, consent- permitted replacements. Existing timestamp-named backups remain untouched.

If the candidate backup directory already exists, preserve it regardless of content and select the first absent numeric suffix in ascending order. Observe candidate collisions and the selected absent path in the batch preconditions. Creation is exclusive/no-clobber; a racing occupant causes conflict/reassessment, never overwrite, merge or an unreported alternate name. Include the chosen directory, descendants and final paths as persistent effects, never transient execution artifacts. Equivalent filesystem states therefore choose the same paths even when assessed on different dates.

For a changed/new manifest or metadata stamp, sample operation time once in assessment and retain it in prepared data. Render the final bytes and hash there; the owning save method must accept/consume that prepared result, not sample now again. Existing non-upgrade callers may prepare immediately before saving through the same owner seam. Unchanged content retains existing timestamps and is not rewritten. Apply after the clock advances must write exactly the prepared bytes or refuse changed preconditions; no silent rerender/time substitution.

Full-plan after.sha256 is exact for its invocation's prepared bytes, not a cross-process replay promise. Independent preview/apply runs may have different new installed_at/updated_at/last_upgraded_at values: the test oracle may compare decoded, owner-declared newly assigned fields with those three names using one sentinel, and recompute comparison digests from that narrow normalization. One additional exact owner/path allowance: managed-skills root /created_at in .kittify/skills-manifest.json, only when that manifest was absent in both independent baselines and is newly created. Sample it from prepared operation time and record the baseline-absence proof and exact JSON pointer. This is not a general created_at ignore rule: pre-existing manifest creation times and mission-meta/historical identity creation times remain exact. Raw bytes/hashes and exact field locations remain evidence. Do not normalize preexisting timestamps, paths, other fields, whole manifests or arbitrary text. Persistent paths/actions/kinds/modes and all nonvolatile content must match across runs. The per-invocation advancing-clock witness permits no normalization.

Provisioning Authority

src/charter/activation/compiler.py retains missing-key/seed policy; pack_manager.py retains pointer-aware target resolution and legacy round-trip serialization; charter_yaml_io.py retains validated section-only rendering. IC-07 extracts pure preparation in those exact files and routes the existing writer through the prepared bytes. Returned charter values are lower-layer bytes, projected activation data and source observations, not PhysicalEffect. The upgrade adapter wraps them and rechecks both pointer/config and target observations before delegating apply. No upward charter import, duplicate YAML serializer in upgrade, write interception as production preview, or same-version incomplete escape for an ordinary missing key. Explicit empty lists stay empty; dangling/unreadable pointers fail rather than fall back to legacy config.

OwnerRequired assessment coverage
Runtime bootstrapExisting package managed-path set, absent/stale runtime, safe removals, version stamps and directories; preserve custom runtime data
Global skills/commandsExisting catalog/templates, health and retired-managed checks even with current markers, whole-agent bundle, permissions and stamps
Command skillsFull canonical command batch, source resolution, sharing/refcounts, command manifest, exact collision/drift checks
Managed skillsSelected catalog/activation, project copies, safe managed-link conversion, backups where existing policy permits, skills manifest, global preparation delegated to runtime
ProfilesAdmission/projection, missing/stale output, exact manifest, orphan pruning including paths absent from expanded statuses; preserve drift
Native/sessionExisting writer-managed sections/config entries, orientation, rules and hooks; preserve unrelated text/JSON keys
Plugin bundlesExisting applicable staged repair only, complete staged output and manifest; never new automatic plugin installation
Upgrade metadata/provisioningExisting stamps and missing-key provisioning, preserve intentional empty activations and unknown config keys

Missing manifests with retained files: canonical rendered-byte equality may establish safe adoption only through the installer's existing explicit content contract; record that proof. Any nonmatching unowned content is preserve/conflict. Never create a placeholder hash of arbitrary disk content and then treat it as managed on the subsequent install. Unknown custom symlinks survive, including names beginning spec-kitty.; only exact ownership permits unlinking. Proven managed but edited prune targets remain drift/consent-required.

Use configured agents exactly; malformed config is incomplete/blocked, not all-known-tools fallback. Preserve required/disabled/advisory policies, profile activation, Amazon Q's user-global exception and shared-root owners.

Phase composition takes concrete projected config/manifest values when provisioning/normalization precedes status assessment. Source lookup can use the installed package's canonical templates on an empty home; never bootstrap to discover them. If an input cannot be read/projected, report incomplete with its owner/reason. Do not silently substitute an unrelated source or infer no work.

Opaque historical migration file lists stay null in their existing migration contract. Mark subsequent dependent assessment incomplete rather than invent an all-files list. Preserve existing migration apply and then reassess actual state before repair. Known independent effects may be reported; incomplete ones cannot execute by replaying the report. Same-version plans must be complete.

An owner observes content/type/mode/mtime, manifest/config source hashes, activation/catalog identity and source template hashes needed for its batch. Before writing, acquire existing lock where available and recheck the whole batch, including parent confinement without following escaping links. A changed source, replaced symlink parent or changed target stops the batch before writes, with precondition_changed. A deliberate re-run reassesses; no automatic force.

Do not weaken command installer's collision checks or shared remove refcounts. Preserve user regions in mixed files. Avoid empty manifest/timestamp rewrites and redundant chmod so same-version second apply has zero persistent churn.

Known drift does not prevent independent missing-file automatic repair, but remains surfaced and preserves current noninteractive unresolved-drift failure. Corrupt required state/source blocks that owner before mutation; no catch-to-empty success. Unexpected I/O failure may occur after partial writes: report actual IDs and error, persist only valid manifest changes, do not emit success for all requested IDs. Existing finalizer derives the final apply outcome; do not invent a second command exit formula. Mission repair failure remains independently reported and retains its existing non-fatal upgrade outcome semantics.

Architectural Guard

Add a narrow AST/import gate over concrete upgrade assessment functions and startup integration. A floor must assert the real CLI entry, phase composer and all selected built-in assessment owners are covered; a missing registry provider cannot reduce coverage silently. Reject direct writer/ensure/persistence calls from assessment bodies and require the named apply boundary. Self-mutation fixtures restore an unconditional root ensure and insert a manifest save in an assessment; both must fail the gate. Do not rely on this syntactic guard for transitive safety: public subprocess snapshots and write-interception controls in acceptance.md remain mandatory.

upgrade-cli.md

Upgrade CLI and JSON Contract

Audience: agentic-framework-core-team. Updated: 2026-09-06. Normative for FR-001-006, NFR-001/002 and existing consent/worktree compatibility.

Verified Legacy Authority

Do not edit or loosen the pinned consumer schema:

kitty-specs/cli-upgrade-nag-lazy-project-migrations-01KQ6YDN/contracts/compat-planner.json

SHA-256: 62ff29b6121919a60ca0cc9e55eaccc7204db202a6d7c1c0f0ccdf0892d6357a. It fixes schema_version=1, additionalProperties=false at root and nested objects, case/decision enums and rendered_human maxLength=1024. Its decision enum includes BLOCK_INCOMPATIBLE_FLAGS; compat/planner.py:_EXIT_CODE_MAP maps it to 2. Current documented use is incompatible --cli/--project. This mission explicitly extends that existing decision's invocation-validation use to invalid --target, without adding an enum/case/key or falsifying schema compatibility.

The existing compatibility Plan is a schema/invocation verdict, not the upgrade write plan. Existing default actual --json returns an upgrade outcome; --project --json currently returns planner JSON without applying even absent --dry-run. Preserve this distinction, not just the common dry-run example.

Flag Matrix

All preview/guidance rows are non-mutating from process entry, no matter TTY, CI, current markers, SaaS toggle or confirmation flags.

Flags / contextBehavior and representation
--dry-run [--project]Human complete assessment; no application
--dry-run --json [--project]Pinned legacy planner; bounded summary of repairs and full-plan hint in rendered_human, no extra keys
--project --json (no --dry-run)Existing planner-only route, still no application; semantic process code
--plan-json [--project] [--dry-run] [--json]New explicit full-plan schema, always preview; redundant --json/--dry-run accepted
default human / --project human, no previewExisting apply; target/schema validation before global bootstrap; normal confirmation
default --json, no --project/previewExisting actual-upgrade outcome JSON EXCEPT too-new schema: pinned legacy compatibility planner, process 5, no bootstrap
--cli [--json] [--dry-run]Existing project-agnostic CLI guidance; --json uses its legacy shape; target/worktree flags retain existing ignored guidance semantics
no project, default (including --dry-run/legacy --json)Existing CLI-guidance fallback
no project, --project legacyExisting project-required error path; no bootstrap
no project, --plan-jsonFull-plan blocked: project_not_initialized, no effects; no fallback to a different JSON shape
--cli --projectFlag conflict; exit 2, no mutation
--plan-json --cliFlag conflict; full-plan blocked, exit 2, no mutation
--yes / --force on previewAccepted but never executes, prompts or authorizes drift/mission repair
--no-worktreesSelect project/global only, no worktree discovery/stamping/application; keep existing scope
--verbose / -v after upgradeHuman detail only; no additional stdout for machine rows
--no-nagExisting suppression; preview suppresses network/persistence even without it

Hidden --agent-choice/--agent-check/--agent-latest precedence is determined by resolved effective intent, not only the presence of preview switches. Resolve a standalone hidden operation as its own intent (hidden options and their ordinary presentation options, including --json); preserve its existing behavior, including legitimate preference writes, even outside a project. Do not relabel that standalone operation as implicit guidance merely because no project exists.

When hidden options accompany a preview/guidance route, reject before hidden dispatch: explicit --dry-run/--plan-json, implicit planner-only --project --json, explicit --cli guidance, or default no-project guidance selected by the ordinary non-hidden route. This includes --agent-latest alone in such combinations, not just --agent-choice. No preference/cache/bootstrap writes or prompts occur. Rejection is process 2: --plan-json selects a full-plan blocked envelope; --json on the implicit preview or guidance route selects the full pinned legacy payload with BLOCK_INCOMPATIBLE_FLAGS/case none/semantic 2; human prints the conflict. Do not run project assessment for this recognized intent conflict. Standalone hidden check/choice controls keep their existing schemas/exits rather than being converted to legacy planner output. Root help/version and completion preserve their existing non-JSON conventions. Parser errors for unrecognized options or missing values remain Click usage errors; no bootstrap on those paths. Recognized semantic errors in machine requests emit one contract-shaped JSON value, not console markup. If --plan-json is present it selects full schema, including incompatible --cli/--project; otherwise --json selects the applicable legacy planner or existing actual-outcome shape. The existing too-new-schema early guard is the explicit exception: default non-dry upgrade --json emits the full pinned legacy compatibility payload (BLOCK_CLI_UPGRADE/project_too_new_for_cli/semantic 5) and process 5 before any bootstrap. Preserve this even when an invalid target independently exists; standalone invalid-target actual-outcome errors remain distinct. New --plan-json always keeps its full-plan envelope, including too-new refusal/process 5; adapt the guard's assessment, never let its old printer bypass the selected envelope.

Target Policy and Independent Blockers

Use one extended upgrade/runner.py validation authority, not lexicographic comparison and not a second renderer comparator.

Current / targettarget.valid / relationPolicy
Known valid / lower PEP 440false / lowerRefuse downgrade
Known valid / equaltrue / equalMay still need repair/migration; not no-work
Known valid / highertrue / higherValid target, only migrations shipped by this executable are selectable
Unknown current / valid targettrue / unknownPreserve existing legacy selection from 0.0.0; compatibility independently assessed
Any / malformed targetfalse / invalidExplicit validation; no traceback or ALLOW
Malformed known current metadatafalse / unknownCorrupt metadata diagnostic; do not coerce to unknown/0.0.0 silently
Omitted targetCompare with installed CLI versionSame semantics in all project modes

No new upper-version restriction is invented. A compatible target cannot override too-new/corrupt schema. A stale schema's BLOCK_PROJECT_MIGRATION is compatibility advice for an unsafe ordinary command; it does not prohibit upgrade itself from performing the needed migrations.

For a standalone invalid target, the exact legacy fields are:

{
  "schema_version": 1,
  "case": "none",
  "decision": "BLOCK_INCOMPATIBLE_FLAGS",
  "exit_code": 2,
  "pending_migrations": [],
  "rendered_human": "Refusing to downgrade project metadata from 3.2.7rc1 to 3.2.6"
}

This is a field projection, NOT a complete payload fixture: retain every required cli/project/safety/install_method/upgrade_hint field from the actual compatibility plan. Keep project.state=compatible for compatible metadata. Malformed target message: Invalid upgrade target version: <target>. Sanitize control characters and bound displayed untrusted values so the whole rendered_human remains <=1024; normal version pairs above stay exact.

Decision precedence for legacy project planning:

1. Recognized mutually exclusive flags -> BLOCK_INCOMPATIBLE_FLAGS, case none, semantic/process 2. Do not perform project assessment to repair a flag error. 2. Corrupt metadata -> BLOCK_PROJECT_CORRUPT, project_metadata_corrupt, semantic 6. 3. Too-new schema -> BLOCK_CLI_UPGRADE, project_too_new_for_cli, semantic 5. 4. Stale/legacy schema -> BLOCK_PROJECT_MIGRATION, project_migration_needed, semantic 4, even if a target is independently invalid. 5. Otherwise invalid target -> BLOCK_INCOMPATIBLE_FLAGS, case none, semantic 2. 6. Otherwise preserve normal compatibility decision/case (ALLOW/ALLOW_WITH_NAG as applicable). Preview suppresses fresh nag/network side effects.

For rows 2-4 plus invalid target, rendered_human contains BOTH the existing compatibility reason and the explicit invalid-target reason (compatibility first, newline separator). This prevents independent blockers being concealed. Do not compute migration selection for invalid target: pending_migrations=[]. Full-plan always exposes target and compatibility separately; no priority translation may erase either reason. Its nested compatibility is the original compatibility assessment, before the legacy-only invalid-target/incomplete-assessment projection. Outer target, decision and diagnostics express upgrade eligibility. Thus target-only refusal may retain nested compatibility ALLOW while outer decision is blocked/code 2; this is not the legacy top-level payload, which must carry the refusal above.

Exit Codes and Assessment Failure

Payload exit_code above is SEMANTIC and never zeroed just because --dry-run is present. Existing legacy project dry-run process normalizes to 0, except the existing non-bypassable too-new-schema guard still exits 5 and flag errors 2. Thus target-only JSON dry-run: payload 2, process 0; target-only human dry-run: process 1, same explicit rejection. Non-dry --project --json uses semantic code (2 for invalid target). Human/actual-upgrade invalid-target remains exit 1 and existing actual-outcome error fields; do not swap that route's schema. Corrupt compatibility JSON dry-run retains normalized process 0 with payload 6; human corrupt assessment fails nonzero. Tests must not infer ALLOW from exit 0.

Full-plan process: 0 when decision=ready, 2 invalid target/flags, 5 too-new, 6 corrupt project metadata, 1 incomplete owner assessment or required ownership/ source conflict. Stronger compatibility/flag failure wins process precedence. Missing project in full-plan is blocked/code 1 with project_not_initialized diagnostic, not corrupt metadata or an invocation flag error. Full-plan includes process_exit_code explicitly, not nested legacy normalization. Migration-only opacity means decision=incomplete, complete=false and code 1, not permission to run its dependent effects.

Required config/manifest/source failures must not serialize empty ALLOW. Full-plan uses blocked (hard failure) or incomplete (opaque migration phase) with complete=false, typed diagnostics and known independent effects only. Legacy has no assessment-completeness field: in those cases project preview uses BLOCK_INCOMPATIBLE_FLAGS/case none/code 2 as a documented invocation assessment refusal unless a stronger compatibility blocker already applies; rendered_human states the incomplete owner/reason and full-plan command. It does not mark project metadata corrupt for an unrelated manifest error. Human prints the same refusal and exits 1. Unknown/custom preserved paths alone are dispositions, not unreadable-state errors.

Full-Plan Representation

--plan-json emits exactly upgrade-plan.schema.json. schema_version is the report schema's own version, not a product release version. No persisted apply endpoint or plan-file execution. Relative paths use roots[] for global/project/ worktree identity; phases and unique effect IDs are deterministic for identical state after root normalization, excluding clock values/new timestamp hashes from ID construction. Backup paths follow the state-derived collision rule in owner-operations.md. after hashes describe exact invocation-prepared bytes; new timestamp fields may differ across independent invocations only under that contract's narrow comparison rule, including root /created_at solely for a new .kittify/skills-manifest.json absent in both baselines. No global created_at normalization, wire hash relaxation or per-invocation normalization is allowed. Raw bytes/hashes and unchanged mtimes remain evidence. Include all persistent owner effects and separately describe execution artifacts/commit policy.

complete=true iff all selected generated/supporting effects can be assessed; decision=ready iff target and upgrade compatibility permit known automatic work. Ready is not a promise of successful I/O or approval of consent-required drift; dispositions describe such work and apply keeps its unresolved-drift exit. blocked is an established refusal; incomplete is a cannot-finish-assessment result. Both prohibit claiming no changes. Invalid target may be complete=true because its refusal is fully known; do not assess or promise bootstrap effects for a refused invocation.

Legacy machine preview includes a bounded summary/hint in rendered_human, not complete repair paths or invented migration entries. Human preview lists full effects from the same assessment; neither renderer performs ownership checks or file discovery. Summary must say 'no migrations' separately from 'no changes'.

Preview does not fetch latest version, write cache or show-time/preferences. For strict legacy preview cli.latest_source, use none/no latest when a cached source is outside the pinned enum (e.g. simple_index); do not relabel it pypi. Use existing distribution-aware install detection/hints and validate the whole legacy payload; do not loosen the schema to accommodate unrelated output drift.

Compatibility Protections

Keep migration selector/history, global custom paths, configured-agent scope, worktree stamp/failure behavior, version pins and authored empty activations. --no-worktrees does not mean no global assets: current root initialization and project surfaces have global dependencies, which full plan must disclose. Keep finalizer's single churn commit excluding independent mission repair; preserve dirty-baseline/manual-review/autocommit policy and existing actual outcome fields. No remote API/event format change.

upgrade-plan.schema.json

{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "urn:spec-kitty:upgrade-plan:1", "title": "Explicit preview-only upgrade --plan-json", "type": "object", "additionalProperties": false, "required": ["schema_version", "mode", "intent", "target", "compatibility", "complete", "decision", "process_exit_code", "roots", "migrations", "effects", "dispositions", "diagnostics", "execution_artifacts", "commit_policy"], "properties": { "schema_version": {"const": 1}, "mode": {"const": "preview"}, "intent": { "type": "object", "additionalProperties": false, "required": ["project_mode", "requested_target", "include_worktrees", "confirm"], "properties": { "project_mode": {"type": "boolean"}, "requested_target": {"type": ["string", "null"]}, "include_worktrees": {"type": "boolean"}, "confirm": {"type": "boolean"} } }, "target": { "type": "object", "additionalProperties": false, "required": ["current", "requested", "relation", "valid", "reason"], "properties": { "current": {"type": ["string", "null"]}, "requested": {"type": ["string", "null"]}, "relation": {"enum": ["lower", "equal", "higher", "unknown", "invalid"]}, "valid": {"type": "boolean"}, "reason": {"type": "string"} } }, "compatibility": { "$ref": "https://spec-kitty.dev/contracts/cli-upgrade-nag-lazy-project-migrations/compat-planner.json" }, "complete": {"type": "boolean"}, "decision": {"enum": ["ready", "blocked", "incomplete"]}, "process_exit_code": {"enum": [0, 1, 2, 5, 6]}, "roots": {"type": "array", "items": {"$ref": "#/$defs/root"}}, "migrations": { "type": "array", "items": { "$ref": "https://spec-kitty.dev/contracts/cli-upgrade-nag-lazy-project-migrations/compat-planner.json#/properties/pending_migrations/items" } }, "effects": {"type": "array", "items": {"$ref": "#/$defs/effect"}}, "dispositions": {"type": "array", "items": {"$ref": "#/$defs/disposition"}}, "diagnostics": {"type": "array", "items": {"$ref": "#/$defs/diagnostic"}}, "execution_artifacts": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["owner", "root_id", "directory", "name_pattern", "purpose"], "properties": { "owner": {"type": "string", "minLength": 1}, "root_id": {"type": "string", "minLength": 1}, "directory": {"type": "string"}, "name_pattern": {"type": "string", "minLength": 1}, "purpose": {"enum": ["atomic_write", "lock"]} } } }, "commit_policy": { "type": "object", "additionalProperties": false, "required": ["project_enabled", "worktrees_enabled", "mission_repair_included"], "properties": { "project_enabled": {"type": "boolean"}, "worktrees_enabled": {"type": "boolean"}, "mission_repair_included": {"const": false} } } }, "allOf": [ { "if": {"properties": {"decision": {"const": "ready"}}}, "then": { "properties": { "complete": {"const": true}, "process_exit_code": {"const": 0}, "target": {"properties": {"valid": {"const": true}}} } } }, { "if": {"properties": {"decision": {"const": "incomplete"}}}, "then": {"properties": {"complete": {"const": false}, "process_exit_code": {"const": 1}, "diagnostics": {"minItems": 1}}} }, { "if": {"properties": {"decision": {"const": "blocked"}}}, "then": {"properties": {"process_exit_code": {"enum": [1, 2, 5, 6]}, "diagnostics": {"minItems": 1}}} } ], "$defs": { "root": { "type": "object", "additionalProperties": false, "required": ["id", "scope", "path"], "properties": { "id": {"type": "string", "minLength": 1}, "scope": {"enum": ["global", "project", "worktree"]}, "path": {"type": "string", "minLength": 1} } }, "state": { "description": "Exact invocation-prepared state. Apply never resamples time. Independent-run timestamp comparison follows owner-operations.md, not a relaxation of this hash.", "type": "object", "additionalProperties": false, "required": ["kind", "sha256", "target", "mode"], "properties": { "kind": {"enum": ["absent", "file", "directory", "symlink"]}, "sha256": {"type": ["string", "null"], "pattern": "^[a-f0-9]{64}$"}, "target": {"type": ["string", "null"]}, "mode": {"type": ["integer", "null"], "minimum": 0, "maximum": 4095} }, "allOf": [ {"if": {"properties": {"kind": {"const": "file"}}}, "then": {"properties": {"sha256": {"type": "string"}, "target": {"const": null}, "mode": {"type": "integer"}}}}, {"if": {"properties": {"kind": {"const": "symlink"}}}, "then": {"properties": {"sha256": {"const": null}, "target": {"type": "string"}, "mode": {"type": "integer"}}}}, {"if": {"properties": {"kind": {"const": "directory"}}}, "then": {"properties": {"sha256": {"const": null}, "target": {"const": null}, "mode": {"type": "integer"}}}}, {"if": {"properties": {"kind": {"const": "absent"}}}, "then": {"properties": {"sha256": {"const": null}, "target": {"const": null}, "mode": {"const": null}}}} ] }, "effect": { "type": "object", "additionalProperties": false, "required": ["id", "phase", "owner", "logical_owners", "surface_ids", "root_id", "path", "action", "before", "after", "reason", "ownership"], "properties": { "id": {"type": "string", "minLength": 1}, "phase": {"enum": ["global_bootstrap", "migrations", "metadata", "provisioning", "manifest_repair", "surface_repair"]}, "owner": {"type": "string", "minLength": 1}, "logical_owners": {"type": "array", "minItems": 1, "uniqueItems": true, "items": {"type": "string"}}, "surface_ids": {"type": "array", "uniqueItems": true, "items": {"type": "string"}}, "root_id": {"type": "string", "minLength": 1}, "path": {"type": "string", "minLength": 1}, "action": {"enum": ["create", "update", "delete", "replace", "retarget", "chmod"]}, "before": {"$ref": "#/$defs/state"}, "after": {"$ref": "#/$defs/state"}, "reason": {"type": "string", "minLength": 1}, "ownership": { "type": "object", "additionalProperties": false, "required": ["kind", "reference"], "properties": { "kind": {"enum": ["manifest", "managed_path", "canonical_content"]}, "reference": {"type": "string", "minLength": 1} } } } }, "disposition": { "type": "object", "additionalProperties": false, "required": ["owner", "root_id", "path", "state", "reason"], "properties": { "owner": {"type": "string", "minLength": 1}, "root_id": {"type": ["string", "null"]}, "path": {"type": ["string", "null"]}, "state": {"enum": ["unchanged", "preserve", "consent_required", "not_applicable"]}, "reason": {"type": "string", "minLength": 1} } }, "diagnostic": { "type": "object", "additionalProperties": false, "required": ["code", "owner", "severity", "message"], "properties": { "code": {"type": "string", "minLength": 1}, "owner": {"type": ["string", "null"]}, "severity": {"enum": ["info", "warning", "error"]}, "message": {"type": "string", "minLength": 1} } } } }