Mission Specification: Upgrade writes project-global state once
Mission Branch: issue-5457-upgrade-project-global-state (planning base and consolidation target) Created: 2026-10-04 Status: Draft Input: Operator brief for spec-kitty/spec-kitty #5457 (P1, milestone "4.0.0 release scope"), grounded in research/code-grounding.md.
Summary
When an operator runs spec-kitty upgrade on a project that has a mission in flight, upgrade writes the project's generated bookkeeping separately in the repository root checkout and in every live lane and coordination worktree, and commits each copy on that worktree's own branch. Each copy gets its own timestamps and migration records. On 4.0.0rc5/rc6 the same run also commits an identical .gitattributes line on every branch. The mission's integration checks then refuse a file the operator never touched:
LANE_AUTO_REBASE_FAILED.
consolidaterefuses the second lane as stale;consolidaterefuses the squash into the target branch withTARGET_BRANCH_CONTENT_CONFLICT;- on missions with a coordination branch, every review start and implement resume fails with
The printed git merge remedy is a no-op, so the operator loops until they hand-edit generated files.
This mission removes the class of defect at its source. Upgrade writes project-global state once, on the repository root checkout, and never writes or commits it on a branch that integrates back into the primary branch. The integration checks also learn, by one declared rule, which tracked paths are primary-owned generated bookkeeping. With that rule, missions already caught in the broken state land without manual edits.
Domain Language
| Term | Meaning in this spec | Do not confuse with |
|---|---|---|
| Repository root checkout | The non-worktree checkout where spec-kitty upgrade runs and where project-global state lives (glossary: docs/context/execution.md#repository-root-checkout). | "main repository" (forbidden phrasing), the main branch |
| Integrating worktree | A linked worktree whose checked-out branch the branch-naming authority recognises as a kitty/mission-… branch (mission, lane or coordination branch, in the new form or the kitty/mission-NNN-… legacy form), or whose branch cannot be read (detached HEAD, which fails safe). Its commits integrate back into the mission's target branch. | A linked worktree on any other branch, including pre-kitty/ legacy NNN-slug[-WP##] branches, which keep today's behaviour |
| Project-global state | Tracked files that describe the whole project rather than one mission's work. Examples: .kittify/metadata.yaml, .gitattributes, .gitignore, .kittify/config.yaml. | Mission artifacts under kitty-specs/<mission>/ |
| Primary-owned bookkeeping | The subset of project-global state that is fully generated by Spec Kitty and marked "do not edit". No work package ever authors it, so each integration merge site may resolve a conflict on it to that site's fixed resolution side (see Integration merge sites). Initially only .kittify/metadata.yaml. | Operator-editable project files (.gitattributes, .gitignore, .kittify/config.yaml), which a WP may legitimately change |
| Integration merge sites | The five places where a lane's work is combined with other work, each with a fixed resolution side for primary-owned bookkeeping: (1) the stale-lane check before consolidating a lane (no merge; the path does not count); (2) the lane → mission merge inside consolidate (the mission side, i.e. the branch being merged into); (3) the mission → target squash or merge (the target side); (4) the lane sync after a coordination commit, i.e. auto-rebase (the incoming coordination/mission side); (5) the dependency-lane merge when a WP's lane is prepared (the dependent lane's own side). Every resolved side is either closer to the primary branch or a lane copy that is never authored and is resolved again at the next integration. | merge in the publish-to-origin sense (never done here) |
User Scenarios & Testing (mandatory)
User Story 1 - Upgrade mid-mission, then consolidate (Priority: P1)
An operator has a lanes mission with two approved lanes. They upgrade Spec Kitty and run spec-kitty upgrade --yes, then spec-kitty consolidate. Both work packages land on the target branch.
Why this priority: This is the reported defect (Path A). It blocks every team that upgrades while a mission is in flight, which includes the main 3.2.x → 4.0 route.
Independent Test: Build a two-lane mission with approved work on a project whose recorded version is older than the current migrations. Run upgrade --yes, then consolidate, through the real CLI. Both lanes' files must be present on the target branch, and the lane and mission branches must carry no upgrade commit.
Acceptance Scenarios:
1. Given a project at an older recorded version with a lanes mission whose two lanes hold approved commits, When the operator runs spec-kitty upgrade --yes, Then:
2. Given that upgraded project, When the operator runs spec-kitty consolidate --mission <slug>, Then it exits 0, both lanes' work is on the target branch, and the target's .kittify/metadata.yaml is the one upgrade wrote in the repository root checkout.
- upgrade commits its changes only on the branch checked out in the repository root checkout;
- neither lane branch gains a commit;
- neither lane worktree is left with uncommitted upgrade changes.
User Story 2 - Single lane, squash into a non-primary target (Priority: P1)
An operator on a 3.2.x project has one lane targeting branch work. They upgrade and accept, then consolidate. The default squash lands the work on work without TARGET_BRANCH_CONTENT_CONFLICT.
Why this priority: This is Path B, the regression from rc4 to rc5 on the main upgrade route.
Independent Test: A one-lane mission targeting a non-primary branch. Upgrade, then consolidate with the default squash strategy, through the real CLI.
Acceptance Scenarios:
1. Given a one-lane mission targeting work and an upgrade run from the repository root checkout on work, When the operator runs consolidate, Then it exits 0 and the lane's files are on work.
User Story 3 - Coordination mission keeps moving after an upgrade (Priority: P1)
An operator has a lanes_with_coord mission. One WP is for_review and another is in_progress. They upgrade. Starting the review and resuming implementation both succeed. The lane sync that follows each coordination lifecycle commit no longer refuses.
Why this priority: This is the round-22 widening. It blocks the mission long before consolidate.
Independent Test: A coordination-topology mission with a live coordination worktree and a lane worktree. Upgrade, then start a review through the real CLI.
Acceptance Scenarios:
1. Given a lanes_with_coord mission upgraded mid-flight, When the operator runs spec-kitty agent action review WP02, Then the review starts, the WP moves to in_review, and no LANE_AUTO_REBASE_FAILED is raised. 2. Given the same project, When upgrade runs, Then the coordination branch gains no upgrade commit.
User Story 4 - A mission already broken by an earlier upgrade recovers (Priority: P1)
An operator upgraded with an earlier release. Every lane and coordination branch already carries its own upgrade commit, with a divergent .kittify/metadata.yaml and a duplicated .gitattributes line. They install the fixed CLI and run the next consolidate, or start the next review. It lands or proceeds without any manual edit, history rewrite or destructive command. The documentation tells them that this is the recovery.
Why this priority: Real projects are already in this state on rc5. A fix that only prevents new damage would leave them stuck.
Independent Test: Reproduce the broken state directly. On each lane and coordination branch, commit a divergent metadata.yaml and the identical .gitattributes line. Then run consolidate (two lanes, and one lane with squash) and a coordination review start with the fixed CLI.
Acceptance Scenarios:
1. Given two lanes that each committed their own divergent .kittify/metadata.yaml and the same .gitattributes addition, and whose lane worktrees exist, When the operator runs consolidate, Then:
This exercises the lane → mission merge site. 2. Given the same branches but with the lane worktrees removed, so the consolidate-time auto-rebase cannot run, When the operator runs consolidate, Then the second lane is not refused as stale for those two paths, and both lanes land. This exercises the stale-check rules directly. 3. Given a lane whose .kittify/metadata.yaml diverges from the target's, When the default squash runs, Then no TARGET_BRANCH_CONTENT_CONFLICT is raised for that path, and the target's copy is kept. This also holds when status.json conflicts in the same squash. 4. Given a coordination worktree and a lane whose .kittify/metadata.yaml copies diverge, When a coordination lifecycle commit triggers the lane sync, Then it resolves that path to the coordination side under a named auto-rebase rule, which the audit commit message names, and does not refuse. 5. Given a WP whose dependency lane carries a divergent .kittify/metadata.yaml, When the operator runs spec-kitty implement for the dependent WP, Then the dependency-lane merge resolves that path to the dependent lane's own copy and does not raise DependencyLaneMergeConflictError.
- both lanes land;
- the target keeps its own
.kittify/metadata.yamlcontent; - the
.gitattributesline appears exactly once.
User Story 5 - A genuine conflict is still refused (Priority: P1)
A WP really edits an operator-owned project file, such as .gitattributes, differently from another lane, or a lane edits a source file another lane also changed. The integration checks still refuse exactly as before, with the same codes and texts.
Why this priority: The new allowance must not become a silent data-loss path. The #4933 / #4978 precedent shows that basename-level bookkeeping exemptions destroyed real work.
Independent Test: The same fixtures as Stories 1–4, but with differing content on an operator-editable path. Each existing refusal still fires.
Acceptance Scenarios:
1. Given two lanes that change .gitattributes with different content, When consolidate runs, Then the second lane is still refused as stale with the existing message and code. 2. Given a lane and a target whose real source file conflicts, When the squash runs, Then TARGET_BRANCH_CONTENT_CONFLICT is still raised, byte-identical to today. 3. Given a lane that conflicts on a non-classified path during the lane sync, Then LANE_AUTO_REBASE_FAILED with "no classifier rule matched" is still raised.
Edge Cases
non-mission branch is not an integrating worktree. It keeps today's upgrade behaviour, including the #4972 version-only alignment and the #2385 commit-on-own-branch behaviour.
as integrating, and so is skipped. This fails safe: writing nothing never diverges. A plain directory under .worktrees/ that is not a git worktree at all (no .git entry) is not integrating, so it keeps today's behaviour.
written under either flag, and still are not.
Integrating worktrees are not stamped (today's upgrade_worktrees_only pass skips them too).
"identical". The stale rule compares the full tree entry (mode and blob). A deletion on one side and an edit on the other is still stale.
path (including a modify/delete conflict), the site's resolution side wins, whether its tree entry is present or absent. Where only one side changed the path, git merges cleanly and that change carries. That is today's behaviour; no rule is needed, because no conflict exists.
kitty/mission-… branch, so upgrade writes and commits there; that is the root checkout, not a linked worktree. At the squash into the target, the target side wins only when the target also changed .kittify/metadata.yaml. Otherwise the mission branch's upgraded copy lands cleanly.
rebase strategy keeps today's refusal. The default squash and the merge strategy are covered.
is unchanged.
branch-naming authority as new-form branches. Pre-kitty/ branches (NNN-slug, NNN-slug-WP##) are not integrating, so they keep today's behaviour. This is a documented residual and no local regex is added for them.
- Worktree on an unrelated branch. A linked worktree under
.worktrees/checked out on a - Detached HEAD or unreadable branch. A real git worktree whose branch cannot be read is treated
--no-worktreesand--dry-run. Behaviour is unchanged. Integrating worktrees were not- Upgrade with no migrations pending. The repository root checkout is only version-stamped.
- Content-identical overlap with different modes or a deletion on one side. This is not
.kittify/metadata.yamldeleted on one side. At a merge site where both sides changed thesingle_branchmission on a protected target. The repository root checkout itself sits on a--strategy rebasefor mission → target. Out of scope. A primary-owned conflict under the- The planning lane (
lane-planning). It runs in the repository root checkout. Its remedy text - Legacy-form branches.
kitty/mission-NNN-slug[-lane-x]branches are recognised by the same
Requirements (mandatory)
Functional Requirements
| ID | Title | User Story | Priority | Status | Delivery | No-op passable? |
|---|---|---|---|---|---|---|
| FR-001 | Upgrade does not write in integrating worktrees | As an operator, I want spec-kitty upgrade to write, stamp and commit nothing in an integrating worktree, so that one upgrade cannot leave divergent copies of project-global state on branches that integrate back. | High | Open | [build] | no — the red test sees a lane commit and a lane metadata write today |
| FR-002 | Project-global state written once | As an operator, I want upgrade's project-global writes to land only in the repository root checkout and be committed only on its branch, so that every integrating branch receives them through integration. | High | Open | [build] | no — paired with FR-001 on the same fixture: the target gains exactly one upgrade commit |
| FR-003 | Non-integrating worktrees unchanged | As an operator with a linked worktree on an unrelated branch, I want upgrade to keep upgrading and committing it as today, so that this fix does not regress #2385 / #4972. | Medium | Open | [ratchet] | yes — positive control: the same fixture's integrating worktree is skipped (FR-001) while the unrelated one is upgraded |
| FR-004 | Primary-owned bookkeeping declared once | As a maintainer, I want the state contract to declare which tracked surfaces are primary-owned generated bookkeeping, matched exactly and anchored at the repository root, and every integration merge site to derive its path set from that one declaration, so that no consumer carries its own filename list. | High | Open | [build] | no — the site tests fail if the declaration is removed |
| FR-005 | Stale-lane check ignores primary-owned bookkeeping | As an operator, I want the stale-lane check not to count overlaps on primary-owned bookkeeping paths, so that a lane is not refused over generated metadata. | High | Open | [build] | no — Story 4 AS-2 (no lane worktree) refuses today |
| FR-006 | Stale-lane check ignores content-identical overlaps | As an operator, I want an overlapping path of any kind whose tree entry (mode and blob) is identical at the lane tip and the mission tip not to make the lane stale, so that an identical change on both sides (such as the same .gitattributes line) is not a refusal. | High | Open | [build] | no — Story 4 AS-2, paired with the Story 5 different-content control on the same fixture |
| FR-007 | Mission → target keeps the target's primary-owned bookkeeping | As an operator, I want a conflict on a primary-owned path during the default squash, or the merge strategy, into the target branch to resolve to the target's tree entry instead of refusing. This applies whatever other derived paths conflict in the same merge, so that a divergent lane copy cannot block landing. | High | Open | [build] | no — the Story 2 / Story 4 AS-3 fixtures raise TARGET_BRANCH_CONTENT_CONFLICT today |
| FR-012 | Lane → mission merge keeps the mission side's primary-owned bookkeeping | As an operator, I want a conflict on a primary-owned path during consolidate's lane → mission merge to resolve to the mission branch's tree entry instead of failing, so that removing the stale refusal cannot turn into an uncoded merge failure. | High | Open | [build] | no — Story 4 AS-1 fails today |
| FR-008 | Auto-rebase rule for primary-owned bookkeeping | As an operator, I want the lane sync (auto-rebase) to resolve a primary-owned path to the incoming coordination or mission side under a named, closed-list auto-rebase rule recorded in its audit output, so that coordination missions keep moving. | High | Open | [build] | no — the Story 3 / Story 4 AS-4 fixtures raise LANE_AUTO_REBASE_FAILED today |
| FR-013 | Dependency-lane merge keeps the dependent lane's primary-owned bookkeeping | As an operator, I want the dependency-lane merge during implement to resolve a primary-owned conflict to the dependent lane's own tree entry instead of raising DependencyLaneMergeConflictError, so that dependent WPs can start after an upgrade. | Medium | Open | [build] | no — Story 4 AS-5 refuses today |
| FR-009 | Genuine conflicts still refused, byte-identical | As an operator, I want every existing refusal (stale lane, TARGET_BRANCH_CONTENT_CONFLICT, LANE_AUTO_REBASE_FAILED, lane → mission merge failure, DependencyLaneMergeConflictError) to fire unchanged for paths the new rules do not cover, so that real work is never silently dropped. | High | Open | [ratchet] | yes — positive control: Story 5 fixtures, which differ from the Story 4 fixtures only in content |
| FR-010 | Recovery documented | As an operator already in the broken state, I want the recovery documented: install the fixed CLI, then re-run consolidate or the review, with no manual edits and no destructive commands. Upgrade-induced stale refusals, which print the no-op git merge remedy, then no longer occur. | Medium | Open | [build] | no — the Story 4 fixtures land with the fixed CLI, and the how-to names exactly those commands |
| FR-011 | Existing tests that pin the defect re-pinned | As a maintainer, I want the tests that assert a per-worktree divergent stamp or a per-worktree upgrade commit on integrating branches re-pinned to the new invariant, while the #4972 alignment tests keep running on non-integrating worktrees. They are not deleted, so that the regression arms stay covered. | Medium | Open | [ratchet] | no — the re-pinned assertions fail on the pre-fix code |
Non-Functional Requirements
| ID | Title | Requirement | Category | Priority | Status |
|---|---|---|---|---|---|
| NFR-001 | No added upgrade work | For a project with N ≥ 1 integrating worktrees, upgrade performs 0 migration evaluations in them, down from N × (worktree migrations) today. | Performance | Medium | Open |
| NFR-002 | Fail-safe classification | Any path not declared primary-owned and not content-identical keeps today's refusal behaviour. That is 100% of the existing refusal tests passing unchanged. | Reliability | High | Open |
| NFR-003 | Quality gates | New and changed code passes ruff check, ruff format --check --force-exclude and mypy with zero issues. Every new function has cyclomatic complexity ≤ 15. Every new branch has a focused test. | Maintainability | High | Open |
| NFR-004 | Deterministic resolution | Resolving a primary-owned path yields byte-identical content across repeated runs on the same inputs. There are no timestamps and no merge markers in the result. | Reliability | High | Open |
Constraints
| ID | Title | Constraint | Category | Priority | Status |
|---|---|---|---|---|---|
| C-001 | Seam-shaped runner change | Changes to the upgrade runner are limited to worktree selection and write placement. How upgrade computes and reports its result (exit status, outcome) is not restructured. That is the separate C6 mission (#4925 / #4893). | Technical | High | Open |
| C-002 | Single authority | The primary-owned fact lives in one declaration, the state contract. No consumer adds a module-level filename list (test_exemption_registry_ratchet.py). The auto-rebase leg is a named rule in auto-rebase's closed, whole-file managed-artifact arm, next to R-COORDINATION-ARTIFACT-THEIRS. It is not added to the per-hunk conflict_classifier.RULES. ADR 2026-05-14-1 is amended to record it. The reconciliation gate's broader .kittify/ bookkeeping predicate (consolidation/reconciliation.py::_is_bookkeeping) is a different fact and stays unchanged. | Technical | High | Open |
| C-003 | No destructive remedies | No new remedy text or documented recovery uses tree-wide git restore, git reset or git checkout -- ., and none rewrites history (#3931 / #5078). | Technical | High | Open |
| C-004 | Existing refusal texts and codes unchanged | Existing refusal codes and messages stay byte-identical. No refusal text is changed by this mission. | Technical | High | Open |
| C-005 | Avoid sibling-mission files | Do not modify cli/commands/implement.py (#5635), core/mission_creation.py (#5634) or finalize lane computation (#5573) unless unavoidable. | Process | Medium | Open |
| C-006 | Red-first through the real entry points | Every defect path gets a failing test through the pre-existing CLI entry point, committed before the fix (charter SO-4, DIRECTIVE_034). A tidy-first enabler, if any, precedes the red test (DIRECTIVE_025). | Process | High | Open |
| C-007 | No new size or ratchet gates | The mission adds no new size gate or shrink-only ratchet, and does not change issue priorities. | Process | High | Open |
| C-008 | Operator-editable files are never auto-resolved | .gitattributes, .gitignore, .kittify/config.yaml and charter/doctrine files are never declared primary-owned. Only content-identical overlaps on them are exempted. | Technical | High | Open |
Key Entities
scope, git class, owner). It gains one fact: whether the surface is primary-owned bookkeeping.
a mission, lane or coordination branch.
managed-artifact arm, with a path predicate, a resolution side and an audit rule id.
resolution side.
- State surface declaration: the existing registry entry for a durable state file (path pattern,
- Integrating worktree: a linked worktree whose branch the branch-naming authority recognises as
- Auto-rebase managed-artifact rule: a whole-file resolution in auto-rebase's closed
- Integration merge site: one of the five places listed in Domain Language, each with a fixed
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: After one upgrade of a project with a two-lane in-flight mission, 0 lane branches and 0 coordination branches carry an upgrade commit, and
consolidatelands 2 of 2 approved WPs (Path A). — [build] · no-op passable: no - SC-002: A one-lane mission upgraded mid-flight consolidates with the default squash on the first attempt, with 0
TARGET_BRANCH_CONTENT_CONFLICTrefusals (Path B). — [build] · no-op passable: no - SC-003: On a coordination mission upgraded mid-flight, 100% of review starts succeed without
LANE_AUTO_REBASE_FAILED(Path C). — [build] · no-op passable: no - SC-004: A mission already carrying per-branch upgrade commits from an earlier release lands with 0 manual file edits and 0 history rewrites. — [build] · no-op passable: no
- SC-005: 100% of the pre-existing refusal tests for stale lanes, squash content conflicts and auto-rebase manual conflicts still pass unchanged. — [ratchet] · no-op passable: yes — positive control: SC-004's fixtures, which differ only in content
Assumptions
root checkout (grounding §4). A lane keeping its pre-upgrade copy of project-global state is therefore harmless until it integrates.
see new .gitignore entries until it integrates. Lanes are short-lived, and the previous mechanism (committing them on the lane) is what caused this defect. The residual is documented in the amendment to ADR 2026-07-07-1. The same applies to new .gitattributes merge-driver mappings (for example the rc5 decisions/index.json driver) and to ignored per-checkout surfaces that upgrade used to refresh inside lane worktrees. A pre-upgrade lane relies on auto-rebase's in-process arms for those artifacts until it integrates.
version plus real lane and coordination worktrees, so that real registered migrations detect and apply. They do not install old CLI wheels in CI. The issue's verbatim reproducers, run against the released old wheels, are the live evidence before and after (decision 01M44580VWDA5VDH5V7ZVZV8DD).
- Commands run inside a worktree already read project-global state (
.kittify) from the repository - Residual window: lane
.gitignoreentries. A lane worktree created before an upgrade does not - Red-first tests simulate the pre-upgrade state through the real CLI entry points: an older recorded
Out of Scope / Related
mission; the two share upgrade/runner.py but not the cause.
for its already-committed skills-manifest.json modify/delete conflict is not covered.
reuse the new predicate later.
two lanes, the printed git merge <mission-branch> is equally a no-op after consolidate's rollback. The only correct remedy merges a sibling lane, which interacts with per-WP attribution. That general remedy defect is deferred to a follow-up issue, and this mission does not change the remedy text. This mission removes the upgrade-induced trigger: overlaps on primary-owned and content-identical paths no longer refuse.
- #4925 / #4893: the upgrade exit status on a no-op or a success. That is the separate C6
- #5473: shares the source mechanism, which this mission removes for future upgrades. Recovery
- #5673:
implementstaging.kittify/config.yamlinto the claim commit. Cross-ref; it should - Stale-lane remedy for genuine in-run sibling overlaps. For a genuine user-file overlap between
- #702, epic #4807, #5229, #5539: cross-references only (see grounding §5).