Context and Problem Statement
ADR 2026-08-12-1 established explicit checkout ownership and exposed
--owned-checkout PATH on exactly two commands: mission create and next.
That decision deliberately left the remaining mission-mutating commands out of
scope, naming the caller-versus-declared-workspace guard as follow-up work
(#3128).
AI agents routinely operate from a task-owned linked worktree, not the
repository-root checkout. With only create and next owned-aware, the rest of
the single-branch lifecycle — prerequisite checks, task finalization, spec
commit, acceptance, task moves, and status transitions — could still only
rediscover the repository-root checkout. From an owned worktree those commands
either refused, or worse, resolved and wrote to the wrong mission surface. The
ownership boundary existed but stopped one command in.
This ADR records the decision to complete that surface for the single-branch
topology, the shared authority that carries it, and the topology restriction it
accepts. It extends — and does not supersede — ADR 2026-08-12-1; the
OwnershipClaim primitive and refusal taxonomy from that decision remain the
foundation.
Decision Drivers
- Finish the owned-checkout lifecycle so a valid single-branch worktree workflow never has to fall back to the repository-root checkout to make progress.
- Reuse the existing ownership, mission-resolution, protection, and fail-closed-metadata authorities rather than inventing a second resolver.
- Keep every flagless (non-opted-in) caller byte-for-byte unchanged.
- Fail closed on every invalid ownership, topology, branch, or index condition — never silently degrade an owned-mode operation to primary-checkout behavior.
- Bound the blast radius: admit only the topology whose write model the owned root actually captures.
Decision
One shared preflight authority
Adopt specify_cli.core.owned_mission — OwnedMission, resolve_owned_mission,
and require_unstaged_index — as the single preflight shared by every owned
lifecycle command. OwnedMission is a validated carrier, not a new resolver:
it composes the authorities that already exist rather than re-deriving placement.
resolve_owned_mission validates, in order and fail-closed:
- repository identity and exact checkout root, via
checkout_ownership.resolve_ownership_claim/error_for_claim(the ADR 2026-08-12-1 primitive); - mission identity, via the canonical
context.mission_resolver.resolve_mission(a corrupt or non-objectmeta.jsonis skipped at indexing time, surfacing a structuredFEATURE_CONTEXT_UNRESOLVED, not a traceback); - mission-directory containment within the claimed checkout's
kitty-specs/; - single-branch topology, via
core.paths.load_meta_fail_closed; - current branch equals the mission target branch (detached HEAD refused) and,
when supplied, the caller's
--target-branchmatches; - protected-destination refusal, via
ProtectionPolicy; - whole-batch path containment before any staging (
OwnedMission.files).
The validated owned root is threaded onward as an explicit effective_root
override through mission-context resolution, the placement seam, acceptance
gates, status transitions, commit routing, and runtime cutover. No opted-in
layer may replace it with get_main_repo_root or another ambient resolver — the
same invariant ADR 2026-08-12-1 set for next.
The affordance now spans the single-branch lifecycle
--owned-checkout PATH is additionally exposed on
agent mission check-prerequisites, agent mission finalize-tasks,
spec-commit, accept, agent tasks move-task, and agent tasks mark-status.
As before, the option names an exact checkout root for that invocation and is
never inferred from CWD, environment, path naming, or the nearest .kittify.
Single-branch only, by construction
Owned mode is restricted to the single_branch topology. A LANES, COORD, or
LANES_WITH_COORD mission opted into --owned-checkout is refused with the
structured OWNED_TOPOLOGY_UNSUPPORTED, and the transactional emitters refuse
with OWNED_TRANSACTION_UNAVAILABLE rather than degrading to an uncommitted
fallback.
The rationale is that coordinated topologies route lifecycle writes through a coordination worktree and branch that the single owned-root override does not model. Admitting them would reintroduce exactly the cross-checkout write hazard the ownership boundary exists to prevent. Fail-closed refusal is the safe scope until a coordinated-topology owned model is designed.
Consequences
Positive
- The single-branch worktree workflow is complete: an owned agent can run the whole lifecycle without ever targeting the wrong checkout.
- One preflight authority governs every owned lifecycle command, composed from the existing ownership/resolution/protection/metadata seams — no second placement resolver.
- Every refusal is structured (
OWNED_*), and every flagless caller is unchanged.
Negative and accepted trade-offs
- The explicit
effective_rootoverride is threaded through many call sites. This duplication is intentional, for the same reason ADR 2026-08-12-1 gave: rediscovery would erase the boundary. (A landing-pass fold routed the omit-the-keyword convention through the singleeffective_root_kwargshelper so the threading has one definition.) - Coordinated topologies are not owned-aware. An operator who used
--owned-checkoutoncreate/nextfor a coord/lanes mission hits a hardOWNED_TOPOLOGY_UNSUPPORTEDrefusal on the later lifecycle commands. This is a deliberate fail-closed scope, not a silent gap. owned_missionstill re-derives some validation (branch/topology probes, a whole-tree containment scan) on each resolve, and re-implements a few checks that canonical helpers also provide. That SSOT/performance cleanup is tracked as follow-up and does not change behavior.
Alternatives Considered
Extend --owned-checkout to coordinated topologies now
Rejected for this decision. Coordinated topologies write through a coordination worktree/branch the owned-root override does not capture; a correct owned model for them needs its own design. Fail-closed refusal is the safe interim scope.
Let each lifecycle command grow its own ownership check
Rejected. That would fork the ownership/topology/branch invariants across
commands and let them drift — the second-resolver hazard ADR 2026-08-12-1
already refused. One shared owned_mission preflight keeps a single authority.
Degrade to the repository-root checkout when owned validation fails
Rejected. Silent degradation is the original cross-write defect. Every invalid condition is a structured refusal instead.
Amendments
2026-09-28 — Validated ownership fact and per-command topology
Mission owned-checkout-lifecycle-authority-01M3M2ZB (issues #3449, #4252, #4867, #5026, #5277).
- The preflight now produces a validated ownership fact.
resolve_owned_missionreturnsmission_runtime.OwnedCheckout, a frozen value object carryingrepository_root,owned_root,mission_dir,mission_slug,topologyandtarget_branch. It can be minted only insidespecify_cli.core.owned_mission, through a private_mint; direct construction raisesTypeError.OwnedMissionandeffective_root_kwargsare deleted with no alias left at mission end. The type lives inmission_runtime, notcore/owned_mission.py, because theruntimeandmission_runtimelayers must consume it without a new outbound edge into the CLI application layer (C-003; enforced bytests/architectural/test_layer_rules.py). - Validated once per command. A command validates ownership at most once, at its entry point, through
cli/commands/_owned_checkout.py(resolve_owned_or_adopt), and hands the resulting fact to every downstream read. Readers never re-validate. Sites that previously re-derived ownership — for examplecoordination/status_transition.pyandcli/commands/accept.py— now consume the fact instead (FR-003, NFR-002). - Supersedes the
effective_rootthreading consequence ("The expliciteffective_rootoverride is threaded through many call sites …", Consequences, Negative and accepted trade-offs). All bare owned-root parameters (census in this mission's plan, Scale/Scope) becomeowned: OwnedCheckout | None. An architectural gate with an empty allowlist forbids the identifiereffective_rootinsrc/outside the org-pack module rule (the unrelatedOrgPackConfig.effective_rootundersrc/charter/**,src/specify_cli/doctrine/**,_doctrine_collect.pyandanalysis_inputs.py), and separately forbids any bare owned-root path parameter under any name (owned_root,owned_checkout,checkout_rootoreffective_roottyped as a path). This is a deliberate no-ratchet conversion, not a leeway ledger (operator decision01M3M65D…). - Per-command topology (supersedes "Single-branch only, by construction" for
next; the lifecycle commands of this ADR keep that restriction). The validated fact records the mission's stored topology, and each command declares its own allowed set.nextacceptsNEXT_OWNED_TOPOLOGIES = {single_branch, lanes, lanes_with_coord, coord}(decision01M3M4GM…), preserving the coordination-topology support it already had rather than newly narrowing it. The lifecycle commands of this ADR (agent tasks status,setup-plan,context resolve,finalize-tasks,move-task,mark-status,spec-commit,accept) keepLIFECYCLE_OWNED_TOPOLOGIES = {single_branch}and still refuse other topologies withOWNED_TOPOLOGY_UNSUPPORTED. The placement-layer duplicate refusal_require_owned_single_branchis deleted, because topology is now decided once, at minting, not re-derived at each consumer. - Owned status reads under
.worktrees/. The canonical coordination surface resolver decides whether a path is refused as a coordination worktree, from the validated fact plus the registered-worktree facts — never from path shape. There is no parallel status-source label; status-source classification stays with that one resolver (FR-013, FR-014, C-002). - Stale repository-root copies. When the repository root checkout holds a copy of the owned mission, owned reads resolve from the owned checkout, never from the repository root checkout's copy, and report
stale_repository_root_copyin JSON output and on stderr (FR-007, decision01M3M2ZX…). - Newly covered commands; typed refusal for
agent action.agent tasks status,agent mission setup-planandagent context resolvegain--owned-checkout.agent action implementandagent action reviewaccept the flag only to refuse it withOWNED_ACTION_UNSUPPORTED, and the refusal namesnext --owned-checkoutandagent tasks move-task --owned-checkoutas the supported path (decision01M3M4GZ…; follow-up #5100). - One claim-commit review base. Every WP review prompt, owned or not, finds its base through one claim-commit helper: the last
claimedevent id recorded in the status log, then the unique commit that introduced it. It fails closed withOWNED_REVIEW_BASE_UNAVAILABLEwhen neither can be resolved. The three duplicate commit-subject matchers this ADR's design otherwise left in place are deleted (FR-010, FR-025, decision01M3M70Y…). - New error codes.
OWNED_CHECKOUT_IS_REPOSITORY_ROOT,OWNED_CHECKOUT_IS_MISSION_WORKTREE,OWNED_ACTION_UNSUPPORTED,OWNED_REVIEW_BASE_UNAVAILABLE,OWNED_COORDINATION_WORKSPACE_UNAVAILABLE.
References
- Foundational decision (extended, not superseded): ADR 2026-08-12-1
- Caller/workspace mismatch follow-up: #3128
- Scoped shadow-workspace design: #3129
- Windows fixture repair (related, still open): #3822
- Owned-checkout lifecycle authority mission:
kitty-specs/owned-checkout-lifecycle-authority-01M3M2ZB/spec.md,plan.md - #3449, #4252, #4867, #5026, #5277