Research: Owned-checkout lifecycle authority
Sources: the pre-spec squad (architect-alphonso, debugger-debbie, python-pedro, planner-priti; see research/pre-spec-options-memo.md), the post-spec squad (reviewer-renata, paula-patterns), and the plan research (architect-alphonso: carrier design; python-pedro: mechanics). All claims were checked against origin/main @ af847be7 / dccf6aa7d unless marked hypothesis.
R-01 Carrier location and shape
- Decision:
- A new
src/mission_runtime/owned_checkout.pyholds a frozenOwnedCheckout(repository_root,owned_root,mission_dir,mission_slug,topology,target_branch), re-exported frommission_runtime. - Construction goes only through
OwnedCheckout._mint(...), guarded by a module-private sentinel.__post_init__checks containment (mission_diris underowned_root/kitty-specs) and thatowned_root != repository_root. OwnedMissionis deleted. Itsfiles()containment helper moves ontoOwnedCheckoutas pure path logic.- Rationale:
runtimeandmission_runtimecannot importspecify_cli.core.OwnedMissionwithout new edges (test_layer_rules.py:116,_baselines.yaml:21).runtime → mission_runtimeis already used by 5 modules (for exampleruntime_bridge.py:197).- MR-1/MR-2 forbid importing submodules from outside the package, so the type must be on the package root.
- Alternatives:
- Keep
OwnedMissioninspecify_cli.core: this forces bare paths in runtime, which is the current defect. - Move it with the old field names: rejected by the operator (decision
01M3M65K…), because the terminology canon rules out bare "primary". - Keep an alias: forbidden by the no-shim burn-down.
R-02 Topology per command
- Decision:
resolve_owned_mission(..., allowed_topologies), withLIFECYCLE_OWNED_TOPOLOGIES={SINGLE_BRANCH}andNEXT_OWNED_TOPOLOGIES={SINGLE_BRANCH, LANES, LANES_WITH_COORD, COORD}.OWNED_TOPOLOGY_UNSUPPORTEDfires when the stored topology is outside the allowed set. Thecoordination_branchrefusal applies only when coordination topologies are disallowed. - Rationale: the operator kept owned coordination-topology
next(decision01M3M4GM…). The e2etests/e2e/test_worktree_owned_root_concurrency.py:405-431relies on it, with P on its target branch, so the branch and protection checks of FR-002 hold (hypothesis: confirmed red-first in IC-05).LANESis included inNEXT_OWNED_TOPOLOGIES(not just the coordination topologies) to preserve today's behaviour exactly: onmain,next --owned-checkoutapplies no topology check at all (it calls only the claim primitive,next_cmd.py:160-176), so a plain lanes-without-coordination owned mission is accepted today. Per the "keep coordination topologies" decision01M3M4GM…, the rewire onto the single validator must not newly narrow that set —nextkeeps accepting every topology it accepts today, soLANESjoinsSINGLE_BRANCH,LANES_WITH_COORDandCOORD.
R-03 effective_root census and migration
- Finding: bare owned-root parameters or fields across
mission_runtime/resolution.py(includingPlacementSeam.effective_rootat:1981),runtime/nextandspecify_cli.src/charteruseseffective_rootfor the org-pack root, a different sense, which is excluded. The exact, current count is the census in plan Scale/Scope (the authoritative measured number); this research note does not restate a separate count. - Decision: convert all bare owned-root parameters and fields (plan Scale/Scope census) to
owned: OwnedCheckout | Nonein this mission. Gate G4/G5 has an empty allowlist. The operator's rationale: "half-implemented work and ratchets have been hurting us for weeks."
R-04 Placement fall-back (root cause of O3/O4/O5)
- Finding:
placement_seam(repo_root, slug)resolves the primary-partition kinds throughget_main_repo_root, so R is returned even whenrepo_rootis P (workspace/context.py:642,694,734;resolution.py:1094).next_cmd.py:176setsrepo_root=effective_root=P, yet reads still land in R.- Decision:
PlacementSeam.owned. When it is set, primary-partition kinds readowned.mission_dirand never callget_main_repo_root.
R-05 Claim commit (FR-010)
1. Take the last to_lane=claimed event for the WP in P's log. 2. Run git -C P log --format=%H -S<event_id> HEAD -- kitty-specs/<slug>/status.events.jsonl, which must return exactly 1 commit. 3. The diff is claim..HEAD -- <owned_files>. 4. On 0 or ≥2 matches, or when there are no owned_files, fail with OWNED_REVIEW_BASE_UNAVAILABLE (a blocked decision).
- Finding (run on HEAD):
move-task --to claimed --owned-checkout Pmakes exactly one commit on P's branch (chore(spec-kitty): status transition WP01). It touches onlystatus.events.jsonlandstatus.json.- No commit hash is recorded (
CommitReceipt.commit_shais discarded,transaction.py:855). - Today's review base uses subject matching (
workflow_executor.py:2236-2279,prompt_builder.py:222-269). It can never match owned claims after the event-only cutover. - #5009's b1a1693bf is still subject matching.
- Decision:
- Alternatives: recording
receipt.commit_shain a follow-up annotation costs a second commit per claim and a schema change, so it was rejected.
R-06 next O5 wedge
- Finding:
runtime_next_steppersists the advance toimplementinside a try (runtime_bridge.py:2288).- Then
_map_runtime_decision→_build_wp_iteration_decision→_wp_iteration_action_and_state→decision._state_to_action:434raisesValueError, outside that try, viaresolve_workspace_for_wpfolding to R. _commit_owned_next_mutationsnever runs.- Decision:
- Carry the fact on
DecideNextContext. - Resolve the WP workspace through the owned arm, which cannot fold to R.
- Resolve before persisting the advance.
- Wrapping the error into
blockedis explicitly not the fix (FR-008).
R-07 Finalize write order and atomicity
1. meta :3381 2. issue-matrix :3417 3. WP frontmatter :3472 4. tasks.md :3478 5. TasksStarted :3506 6. canonical events and bootstrap :2980-2982 7. lanes.json :2992 8. acceptance matrix :3009 9. commit :3020
- Finding: the write order in
mission_finalize.pyis:
Refusals after writes are ownership manifests :3489, stale-canceled :3501, lane cycle and pins :2992, and O6's status-surface refusal via status/bootstrap.py:151. Dependency-graph cycles and invalid refs are already atomic (_validate_dependency_graph at :3431 runs before any WP write, verified clean).
- Decision:
- Build the whole finalize plan in memory first, reusing the
--validate-onlypath (INV-6), then apply all writes. - Amend spec US4-AS3 to use a lane dependency cycle (WP01→WP02→WP03 with WP01 and WP03 sharing an owned file) as the red-on-base refusal.
- Superseded by origin/main's #5100 (merge
ff666bbbe): asingle_branchmission — the only owned lifecycle topology — computes exactly one repo-rootlane-planninglane (Invariant T-1), so the lane-dependency-cycle refusal is unreachable inside an owned checkout. The atomic-finalize guarantee (FR-015 / US4-AS3) is now proven by the single-lane finalize test, with the stale-canceled, planning-pin-orphan and issue-matrix refusals keeping the post-write-refusal coverage.
R-08 #4867 reproduction
- Finding:
_resolve_owned_coordination_workspace→CoordinationWorkspace.resolve→_has_stale_worktree_registration(coordination/workspace.py:163) runsgit worktree list --porcelainwith stderr not captured.- Exit 128 is re-raised as
DecisionGitLogUnavailable(runtime_bridge.py:389), which nothing catches. - Truncating any registered worktree's
.git/worktrees/<name>/commondirmakes git printfatal: failed to read …/commondir: Success(verified with git 2.43; a missing file does not reproduce it). - Decision:
- Capture stderr and raise a typed
CoordinationWorkspaceUnavailable. - The runtime maps it to a
blockeddecision withOWNED_COORDINATION_WORKSPACE_UNAVAILABLE. - The red-first repro: owned
lanes_with_coordcreate, remove the coordination worktree dir, add a sibling worktree with a truncatedcommondir, thennext --owned-checkout P.
R-09 Flagless adoption
- Finding:
missions/operation_context.py:69-116adoptsget_status_read_root(cwd)without validation. Its sole caller isagent/context.py:148.- The other
get_status_read_rootuses are lane-worktree reads (#984) and a branch identity read; neither adopts mission data. - Decision:
adopt_owned_checkout(repository_root, cwd, handle, *, allowed_topologies)inowned_mission.pyreturnsNonewhen: - the caller is R or belongs to a different repository;
- it is a coordination worktree (
surface_resolver.classify_worktree_topology); - it is a lane worktree (
lanes.json/ workspace context); - or the validator raises.
Otherwise it returns the fact. operation_context.py is deleted.
R-10 Status surface under .worktrees/
- Finding:
status_service.read_event_log:162refuses any.worktreespath shape for repository-root-labelled reads. #5009'sStatusReadSource.OWNED_CHECKOUTis a parallel source (C-002). - Decision:
EventLogReadContract.owned.- The guard delegates to
surface_resolver.primary_read_targets_coord_worktree(path, *, owned), which returns False whenownedis set and the path is underowned.owned_root. - The label stays unchanged.
- The fact already proves registration and branch, so no extra git calls are made (NFR-002).
R-11 Composition policy
- Finding:
_dn_composition_dispatch(runtime_bridge.py:2020-2087) readsPackContext.from_config(ctx.repo_root). Through the real CLI that is already P (hypothesis); under the new call shape (repo_root=R) it must readowned.owned_root. - Fixture: R's
.kittify/config.yamlhasmission_type_activations: []while P has a provisioned test charter (validated by #5009 1be5352ee).
R-12 Stale-copy warning channel
- Finding: only
context resolvehas awarningslist (mission_runtime/context.py:321).agent tasks status,setup-planandnexthave none. - Decision:
- A top-level JSON field
stale_repository_root_copy: {"path", "mission_id"} | nullon all five owned read payloads. - The human text is appended to
warningswhere that list exists, and printed on stderr in human mode. - Detection compares the repository-root placement against
owned.mission_dir, with no git calls.
R-13 CLI plumbing
- Finding: there is no shared
--owned-checkoutoption or refusal emitter. Each command declares the option inline (tasks.py:782,916,mission_finalize.py:3237,mission_check_prerequisites.py:563,next_cmd.py:132), and the envelopes differ. - Decision:
- A new
cli/commands/_owned_checkout.pywithOwnedCheckoutOption,resolve_owned_or_adoptandemit_owned_refusal. - The emitter keeps each command's existing envelope keys.
- The golden contracts are updated for the new flags.
R-14 Repository root checkout passed as --owned-checkout
- Finding:
resolve_ownership_claimreturns OWNED for the repository root itself (checkout_ownership.py:217-223). - Decision: refuse it with the new code
OWNED_CHECKOUT_IS_REPOSITORY_ROOT, added to the NFR-004 registry.
R-15 #5009 carry map
The keep/adapt/drop disposition of each commit is recorded in plan IC-13. Four test commits carry cleanly and are red on HEAD:
- a37e9ee39: FR-016 (genuine red);
- edaa9cd83: red only by
TypeError, needs adaptation; - 1be5352ee: composition red; the review assert must be rewritten to the R-05 contract;
- 4ff6ff0c0:
test_finalize_and_read_owned_mission_below_worktreesis a genuine red.test_owned_contract_validates_root_and_missionis dropped (C-002). The coordination-rejection test is kept, and a registered coordination worktree case is added.
6d682dce3 is dropped.
Adversarial evidence
There is no dependency decision, so the supply-chain adversarial pass does not apply. The design was challenged by three squads (pre-spec, post-spec, plan research). Their contested findings and dispositions:
- #4867 vs single_branch: changed (per-command topology).
- Flagless behaviour: changed (validated adoption).
- Owned implement/review path: changed (
next+move-task). - Claim-commit definition: changed (R-05).
- US4-AS3 refusal choice: changed (R-07).
- Parallel status source from #5009: accepted (dropped).
R-16 Post-plan squad folds (randy-reducer, paula-patterns, debugger-debbie)
- Topology single authority.
_require_owned_single_branch(resolution.py:1505,1596,2361) is deleted. Topology is decided at minting only. - Status shape guard. It runs at six sites in
status_service.py, and all six route throughsurface_resolver.transaction.py:644's raw.worktreescheck moves tois_under_worktrees_segment. - Review-base duplication. There are three subject matchers (
workflow_executor.py:2259,prompt_builder.py:255,core/worktree_topology.py:125), broken for every mission since the event-only cutover. They are unified intoclaim_commit_for_wpfor all review paths (operator decision01M3M70Y…). - CLI duplication. There are eight inline
--owned-checkoutdeclarations and a separate_emit_checkout_ownership_error. They are unified onto_owned_checkout.py, which builds onjson_contract.json_error. - Path safety.
OwnedCheckout.files()must keep the symlink and loop defence (kernel.resolution.resolve_rejecting_loops). - Adoption.
tasks_status_cmd.py:224's cwd root was falsified as a second adoption path, because the handle resolves against R first. The mission-surface-conflict refusal for different mission ids is preserved insideadopt_owned_checkout. - Sizing. About 400 edit sites and about 26 test files; see the plan's Scale/Scope. Staging is top-down, with transitional surfaces (the six shared seams plus every other function marked TRANSITIONAL(WP18)) deleted in the closing WP (operator decision
01M3M713…). - Gate. G4/G5 were rewritten as an identifier ban plus a named carrier-field rule, and G1 covers all reference forms. G2's floor was corrected:
cli/commands/accept.pyis on the allowed path. - R-snapshot. It excludes exactly P's subtree, and covers the shared lock root,
SPEC_KITTY_HOMEand cwd ∈ {R, P, elsewhere}. - #4867. Only a zero-byte
commondirreproduces it (a missing file or a lone newline gives rc 0). The stderr wording depends on the C library, so tests assert onerror_codewith a pre-assertion that the injected fault really breaks git, plus a subprocess-seam injection. - FR-022. The coordination-topology owned
nextcoverage was nightly-only, so an in-process per-PR twin is added. - Complexity.
accept(59) and_create_mission_core_impl(40) are the only over-limit functions in their per-file-ignored files. Both are fully decomposed and their C901 ignores removed (operator decision01M3M718…).