Research: Finalize re-pins an orphaned planning_commit_sha (#4827)
Phase 0 consolidation. Grounding by two profile-loaded lenses (alignment + scope) and stress-tested by two post-spec adversarial lenses (invariant + regression). No new third-party dependency is introduced, so the supply-chain planning gate is N/A (documented, not silently skipped).
Decision D1 — Orphan discriminator keys off the target-branch tip, not the lane HEAD
- Decision: classify the recorded
planning_commit_shaagainst the planning target-branch tip using two git predicates:git merge-base --is-ancestor <recorded> <tip>(reachability) andgit cat-file -e <recorded>^{commit}(object presence). Four classes: captured / current-advanced / orphaned (present, unreachable) / foreign (absent). - Rationale: the regression lens proved that the allocator's existing no-op gate
is-ancestor <pin> <lane HEAD>(worktree_allocator.py:612-621) is a different question — it short-circuits when the pin is reachable from the lane HEAD, and the merge (hence any conflict) only runs when it is NOT, which is the normal healthy state of a fresh coord lane (#2993). Keying orphan detection off lane-HEAD reachability would misfire on every healthy allocation. The rebase orphans the pin relative to the target branch, so the target tip is the correct discriminator, and it matches finalize's own classification. - Alternatives considered: lane-HEAD reachability (rejected — see above); object-presence alone (rejected — cannot separate advanced from orphaned).
Decision D2 — Re-pin target is the target-branch tip; do NOT content-match (C-004)
- Decision: re-pin to
_capture_target_branch_tip(target_branch), the same value the fresh-capture (tasks_finalize.py:371) anddoctor mission-state --fix(migration/mission_state.py:1586) writers already use. - Rationale (adversarially confirmed by the invariant lens): on coord/lanes topology, WP-implementation commits are confined to lane branches (
execution-lanes.mdrule 5: merge flowslane → mission → target;branch-target-routing.md: code → lane branch, planning/lanes.json/meta.json→ target branch). So re-pin-to-tip cannot drag sibling-lane WP work into a lane's merge base — the over-merge the scope lens feared does not materialize. Moreover the rebase already re-parents the planning commit onto the advanced base, so a content-matched pin would descend from the same base (content-match does not avoid base drift) and would only make the re-pin path diverge from the other three writers, violating C-002. Content-matched (patch-id) precision is deferred to the durable-artifact-SHA work (context #2897). - Alternatives considered: content/patch-id match (rejected — no benefit here, diverges writers, must handle squash/fixup/drop); blind
HEADof the current checkout (rejected — must be the target branch tip specifically). - Disposition:
accepted.
Decision D3 — Explicit --allow-orphaned gate; default fails closed only on a PROVEN orphan
- Decision: default (no-flag) finalize fails closed on a proven orphaned pin (present + unreachable + tip capturable) with an orphan-specific diagnostic naming
--refresh-planning-commit --allow-orphaned. The re-pin itself requires the explicit--allow-orphanedoperator assertion. Bare--refresh-planning-commitkeeps #4141 advance-only refusal (message retains the substring "not an ancestor"). Foreign objects are refused even with--allow-orphaned. - Rationale: a present-but-unreachable pin is topologically indistinguishable from a benign rebase orphan vs a genuinely divergent side-branch pin (the #4141
test_refresh_refused_when_recorded_sha_not_ancestorfixture is exactly this shape). The tool must not auto-guess; the operator asserts "I rebased; re-point to the live tip." The--allow-orphaneddefault False is what keeps the #4141 refusal test green unmodified. - Alternatives considered: default auto-repair on orphan (rejected — cannot distinguish benign rebase from divergence without operator intent); extend bare
--refresh-planning-committo accept orphans (rejected — breaks the #4141 refusal contract/test). - Disposition:
accepted.
Decision D4 — Default path DEGRADES to preserve on non-git / uncapturable-tip / foreign
- Decision: the no-flag default fails closed only for a proven orphan against a capturable tip. When the tip is uncapturable, the workspace is not a git repo, or the object is absent (foreign), the default degrades to the historical #3311 preserve.
- Rationale (regression lens HIGH-2):
test_issue_3311_finalize_rewrites_active_lanes.pyseeds a synthetic SHA (deadbeef…, absent) in a non-git tmp workspace and asserts preserve on a plain run. A blanket fail-closed would flip that test → NFR-001 regression._capture_target_branch_tipalready returnsNonegracefully (never raises) — the degrade path reuses that. - Disposition:
accepted.
Decision D5 — Centralize detection in the shared helper; reconcile all consumers (Finding 2)
- Decision: orphan detection lives in the single shared
_merge_recorded_planning_commithelper (coveringworktree_allocator.py:419/:482/:562 andimplement_support.py:352).check_claim_ancestry(implement_support.py:497) and_mt_resolve_owned_review_base(tasks_move_task.py:697) are reconciled with the same classification so an orphan never surfaces as three different unreconciled diagnostics. The helper gains a target-branch ref argument (C-006) to run the classification. - Rationale (invariant lens Finding 2):
_mt_resolve_owned_review_basereads the pin viarev-parse --verify <sha>^{commit}, which SUCCEEDS on an orphan (object present) → the owned-review diff is silently computed against a dead base — a correctness bug, not just a UX gap.check_claim_ancestryrefuses the claim without naming the recovery. Both must recognize the orphan. - Disposition:
accepted.
Decision D6 — NFR-003 admits an already-allocated-lane catch-up merge (Finding 1)
- Decision: idempotence is asserted for finalize re-reads and not-yet-allocated lanes only. An already-allocated lane (worktree merged the OLD pin) will, on its next
implement, perform a legitimate catch-up merge of the re-pinned live planning state; a conflict there is a genuine content conflict (live base), the operator's to resolve — distinct from the pre-re-pin stale-pin dead-end. - Rationale (invariant lens Finding 1): the re-pin heals the finalize field and fresh allocation, but a lane materialized before the rebase merged the old pin; after re-pin the new pin is reachable (not orphaned), so the allocator's lane-HEAD merge runs a real 3-way merge to bring the lane up to the live planning state. This is correct-by-design catch-up, but it is a mutation — NFR-003's original "no further mutation" was false and is corrected. The stale-pin error only fires while the pin is orphaned; once re-pinned, genuine conflicts stay genuine.
- Disposition:
accepted(scoped + documented; not folding a separate reuse-path repair — the catch-up merge is the correct behavior).
Decision D7 — FR-010 folds the #4178 preserve-WARN false-positive + print-before-write
- Decision: correct the preserve-path drift WARN so it (a) does not point an orphan at the bare
--refresh-planning-committhat then refuses, (b) does not fire on the tool's own finalize bookkeeping commit, and (c) prints the decision AFTER thelanes.jsonwrite. - Rationale (regression lens CONFIRMED-6): verified the ordering —
_compute_and_write_lanescaptures the recorded SHA before_commit_finalize_artifactsadvances the tip, sobranch_tip (T+1) != sha (T)fires the WARN on every execution-begun no-flag run even absent an operator amendment (the existing test masks it by mockingcommit_for_mission). Same lines FR-009's report touches → fold together. - Disposition:
accepted(opportunistic, same code region; #4178 stays open for its docs-whitespace bullet).
Confirmed safe (captured so a later reviewer does not re-litigate)
- FR-009/ADR-2026-07-29-1 provenance: the orphan re-pin is a fresh, equally-frozen snapshot into the same
lanes.jsonwrite — the ADR's own "Negative/accepted trade-offs" already sanctions a re-finalize recording a new value. No second commit, no allocate-time live read. NFR-004 holds: only the added planning ancestor changes; lane parents (coordination/mission branch) are untouched. target_branchidentity note: the scenario type-checks whentarget_branchis the branch that was rebased (where planning commits live);merge_target_branchis the separate real landing. A third party advancing a sharedtarget_branchbetween rebase and re-pin is bounded identically to what fresh-capture/doctor-fix already accept — not flip-forcing.
Not folded (noted for the PR body, per C-005)
- #2273 (first-class "rebase mission onto moved base"), #3936 (guided dependency-lane conflict resolution), #2897 (doctrine: no bare SHAs in durable artifacts). Fixing #4827 shrinks #3936's false-conflict surface as a side effect; not a folded deliverable.