Context and Problem Statement
Lane branches and worktrees are created from the Mission slug and lane id
alone (lanes/worktree_allocator.py::predict_lane_worktree, which calls
lane_branch_name(slug, lane_id) and worktree_path(..., mission_id=None, ...)). Before this mission, six read sites re-derived a lane's name using
the lanes manifest's mission_id instead of reusing the created name: the
reconciliation claim, pre-interrupt lane-tip capture, the pre-mutation safety
preflight, cleanup's residue scan, the lifecycle_sync multi-candidate probe,
and acceptance's lane source-root resolution.
For a Mission whose slug-derived name and identity-derived name differ, this produced four divergent shapes:
- a legacy
NNN-slugMission whose identity was backfilled after its lanes were already created under the slug-only form; - a slug that embeds a mid8 that differs from the Mission's recorded identity's mid8;
- a lanes-manifest identity that is not a valid ULID, ≥ 8 characters (composed a name creation never produced); and
- the same, < 8 characters (crashed inside
_mid8).
spec-kitty merge on any of these refused with "no approved lane resolved any
commits" — a message that named no branch and gave the operator nothing to
act on (issue #5108) — or crashed outright on the < 8 character shape.
Decision Drivers
- Single canonical authority. Exactly one decision may name a lane's branch and worktree, and it must be the same decision that creates them.
- No probing (ADR
2026-07-01-1). A read site may not fall back to trying several candidate names; a degraded input is migrated or refused, never rescued by a second code path. - No manifest schema change. The lanes manifest already stores what
creation needs (
mission_slug,lane_id); no new per-lane name field should be required to fix a divergence that already-created branches make irrelevant. - Layer direction preserved. The fix stays inside
specify_cli; no new upward import and no growth of the shrink-only outbound ledgers. - Close the defect class by construction. A future contributor must not be able to reintroduce identity-aware lane naming by accident — the public surface itself should make it inexpressible, backed by a gate.
Considered Options
- A — Extend the existing creation-side authority (chosen): remove the identity parameter from the public lane-naming surface so the created name is the only name any caller can compose, and reroute every read/match site through it.
- B — Persist per-lane branch names in the lanes manifest. Rejected (violates C-002): adds a schema field to carry information already recomputable from data the manifest already holds.
- C — Move naming into a lower layer (e.g.
mission_runtimeorkernel). Rejected: lane naming is aspecify_cli-owned decision; moving it down would grow the shrink-only outbound ledgers for no behavioral gain and split one authority across two layers. - D — Keep a private identity-aware composer to satisfy any test or caller that still wants an identity-derived name. Rejected: this is exactly a second naming authority, and it is the thing FR-002 and C-004 forbid — it would keep the defect class alive under a different name.
- E — Probe candidate names at read time (try the slug-only form, then
the identity-derived form, use whichever branch exists). Rejected by
C-004 / ADR
2026-07-01-1: multi-candidate probing is the specific anti-pattern that ADR forbids, and it would leave the ambiguity in place for every future reader instead of resolving it once.
Decision Outcome
Chosen option: A. The lane-naming authority is extended, not duplicated.
- Signatures.
lane_branch_name(mission_slug, lane_id, planning_base_branch=None),worktree_dir_name(mission_slug, *, lane_id)andworktree_path(repo_root, mission_slug, *, lane_id)no longer accept a Mission-identity parameter. Their bodies are the pre-existingmission_id is Nonebranches, byte-identical.mission_branch_name,mission_branch_name_required,coord_branch_nameandresolve_branch_nameare unaffected — they name the Mission and coordination branches, a distinct decision that still takes the identity. predict_lane_worktree(lanes/worktree_allocator.py) remains the single placement decision every creation site calls.- Parsers, not probes. Two additive parsers in
lanes/branch_naming.pyreplace ad-hoc name testing, never by testing whether a name exists on disk or in git:parse_lane_worktree_dirrecognizes a lane worktree directory by shape alone (a right-anchored match on the lane-id grammar) — it is slug-free and never confirms that the recovered slug names a real Mission;lane_id_for_worktree_dirrecognizes by recomposition — it accepts a candidate only when re-composing it withworktree_dir_namefor the caller's known Mission slug reproduces the input byte-for-byte, which is what rejects the057-foovs057-foobarprefix trap. A caller that already knows the expected Mission slug must use the recomposition form.is_lane_branchnow also accepts the plain-legacy grammar (kitty/mission-<slug>-lane-<id>with no numeric prefix and no mid8). Every lane-id fragment (_LEGACY_LANE_RE,_PLAIN_LEGACY_LANE_RE,_NEW_LANE_RE, and both new parsers) is built from one private grammar fragment,_LANE_ID_RE = "lane-[a-z]+", so there is exactly one lane-id shape to keep in sync. This mission also widens_LANE_ID_REfrom the base branch's single-letterlane-[a-z]tolane-[a-z]+, so a multi-letter lane id (lane-aa) is recognized too — a recognition-side change, not a minting-side one (see Residuals). - The reconciliation claim, pre-interrupt lane-tip capture, the pre-mutation safety preflight, and post-merge cleanup all resolve the lane branch and worktree that creation actually produced. An approved, non-canceled lane whose created branch does not exist in git now yields a named refusal (the branch name appears in the message) instead of an empty, generically reported commit set. The FOLD-3 git-probe-error tolerance is unchanged.
- The gate.
tests/architectural/test_no_worktree_name_guess.pyis extended with four legs: a signature leg (test_seam_signature_carries_no_mission_id, nomission_idon the lane surface), a compose leg (test_no_lane_name_compose_outside_authority, an interpolated lane-token f-string composed outside the seam), a match leg (test_no_lane_name_match_outside_authority, regex/startswith/endswith/split/format/.join/etc. over lane tokens), and a def-use leg (test_no_lane_name_def_use_outside_authority, tracing every branch/path argument that reaches a git mutation or lookup back to a naming-authority call). Each leg carries its own shrink-only, capped allow-list (_SIGNATURE_ALLOWLIST_FILES0,_COMPOSE_ALLOWLIST_FILES1,_MATCH_ALLOWLIST_FILES1,_DEF_USE_ALLOWLIST_FILES1), combined into one shared, file-qualified union (_NEW_LEG_ALLOWED) that every leg consults — self-tested against injected offenders in each idiom.
Consequences
- Golden re-pin (the NFR-001 reading). "Every golden describing a
created name stays byte-identical" — not every golden that happens to
encode an identity-injected name. Lane and worktree golden rows that
encoded a name creation never produces (for example the row labelled
legacy-NNN-with-mid8-1589in theGOLDEN_ROWStuple intests/lanes/test_branch_naming_seam.py, and several_PARITY_CASESrows intests/specify_cli/lanes/test_branch_naming_ssot_entrypoint.py) are re-pinned to the created name, with new rows asserting that each divergent shape composes that same created name. Mission-branch, coordination, and Mission-dir golden columns are unchanged. - Mission-branch preservation on re-finalize (FR-011). Re-finalizing
tasks keeps
lanes.json's recordedmission_branchbyte-identical; only a first-time finalize defines it. Consumers that used to recompose the Mission branch from the identity when the manifest lacked a value now prefer the recorded value first, and convert an invalid-identity failure into a typed refusal (LaneComputationError/BranchIdentityUnresolved) namingspec-kitty migrate backfill-identityas the remedy, rather than crashing. - Resume refusal on coordination topology (FR-005, H5).
spec-kitty merge --resumerefuses, namingspec-kitty merge --abortas the remedy, when a coordination-topology merge's persisted pre-interrupt lane-tip record lacks a tip for any non-canceled, non-planning lane under its created name. A merge interrupted under a pre-mission release therefore cannot resume silently under the armed guard — this is the intended upgrade behavior, not a regression. - The reversal of the #1899 premise. The naming gate's own docstring
described the single canonical seam as composing every name "keyed on the
declared
(slug, mission_id)". That premise is reversed: the seam is now keyed on(slug, lane_id)only for lanes: the Mission identity is not an input to lane naming at all. Only Mission and coordination branch naming still take the identity.
Residuals / Follow-ups
Recorded, not fixed here (ADJ-7 and the plan's Risk 6):
- The
kitty/mission-{slug}*recovery-enumeration prefix over-match (a057-fooMission's enumeration also matches057-foobar) is a separate, pre-existing defect, filed as a follow-up. - A second lane-creation path exists in the review-workspace flow
(
cli/commands/agent/workflow.py); it is naming-compliant today but was not folded into the gate's def-use leg, and is noted for a future audit. migration/mission_state.py's rebuild path can construct lane state without reading a prior manifest; whether that path can ever diverge from the naming authority is unverified and left as a follow-up.- The recognition-side grammar (
_LANE_ID_RE = "lane-[a-z]+") already accepts multi-letter lane ids (e.g.lane-aa); the real ceiling is on the minting side,lanes/compute.py::_next_free_lane_id, which walkschr(letter)from"a"with no wraparound — after"z"it mints"lane-{", which no lane regex matches. No Mission has needed more than 26 lanes, but a minter fix (not a grammar change) would be required past that ceiling. - C-007 — legacy retirement tension (recorded, not decided). This mission keeps legacy (pre-3.2.x) Missions working, including the numeric-prefix and plain-legacy lane grammars. Whether pre-3.2.x legacy Mission support should be retired at all is tracked separately (see #2463) and is explicitly not adjudicated by this ADR.
Links
- Spec:
kitty-specs/lane-branch-naming-authority-01M3EVC4/spec.md - Plan:
kitty-specs/lane-branch-naming-authority-01M3EVC4/plan.md(PD-1 through PD-15) - Issues: #5108, #5113
- No-probing ADR:
2026-07-01-1