Skip to content

Handle a stale completed branch when creating an ODW addendum worktree (#42)#52

Open
pandalump wants to merge 2 commits into
mainfrom
issue-42-addendum-worktree-collision
Open

Handle a stale completed branch when creating an ODW addendum worktree (#42)#52
pandalump wants to merge 2 commits into
mainfrom
issue-42-addendum-worktree-collision

Conversation

@pandalump

Copy link
Copy Markdown
Collaborator

Summary

Normal addendum selection halted when git worktree add -b roadmap-<id>-addendum collided with a stale, already-completed addendum
branch of the same deterministic name left behind by a prior run. This makes
createWorktree detect the collision and reclaim the leftover when it is safe,
instead of failing the run.

What changed

  • src/workflows/df12-build-odw/worktree-collision.ts (new): a pure,
    I/O-free decideWorktreeDisposition helper that turns host-observed git
    facts into create | reclaim | fail. Kept out of the formally-verified
    recovery-decision twin. Rule: reclaim only when the branch tip is a merged
    ancestor of origin/BASE and any worktree on it is clean; fail closed on
    unmerged commits or a dirty worktree. candidateRoadmapComplete only
    corroborates a reclaim and never licenses discarding work.
  • main.ts: createWorktree now probes for the existing branch
    (non-throwing rev-parse --verify), gathers collision facts only on that
    rare path (merged-ancestor, worktree presence/cleanliness, lazy roadmap
    completeness), and executes the disposition with sandbox-permitted git
    commands only (worktree prune, branch -f, plain worktree add, reset --hard origin/BASE), re-running the existing HEAD-versus-base verification.
    The no-collision path is unchanged. A fail disposition preserves today's
    failed/worktree halt semantics.
  • Docs: ADR 003 records the tightly-scoped destructive-operation policy;
    roadmap item 4.2.1 gets a note that on-demand reclaim of one deterministic
    name is an exception while the general discard-branch sweeper stays
    deferred.
  • Tests: exhaustive pure unit coverage of the decision table
    (tests/modules/worktree-collision.test.ts); a withStaleAddendumBranch
    option added to tests/fixtures/recovery-repo.mjs to model the real-git
    merged-and-clean leftover.
  • Regenerated the committed artefact workflows/df12-build-odw.js.

Testing

make workflow-build succeeds. Commit gates (check-fmt, lint, typecheck,
module and artefact suites, workflow-freshness, verify-modules) are pending
and will be run by the orchestrator.

Deviations from the CodeRabbit plan

  • Kept createWorktree inline in main.ts (Phase 2), as the plan directed; the
    already-present-but-unused imports (parseWorktreeList,
    candidateRoadmapComplete, roadmapTaskIndex) confirmed this intent.
  • Phase 3's end-to-end artefact-level reclaim test is not added: main.ts is
    not host-importable (top-level ambient args), so createWorktree cannot be
    driven from a bun/node --test harness without the full ODW runtime. The
    pure disposition unit test is the primary logic guard (as the plan itself
    labels it), and the withStaleAddendumBranch fixture models the git state for
    any future runtime-level coverage.

Closes #42

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @pandalump, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Summary

  • Fixes issue #42 by safely handling collisions with stale roadmap-<id>-addendum branches.
  • Adds deterministic create, reclaim, or fail-closed fail decisions via the pure decideWorktreeDisposition helper.
  • Reclaims branches only when merged into origin/BASE and associated worktrees are clean; unsafe collisions preserve existing halt behaviour.
  • Refactors provisioning and error reporting, including verification that the created worktree HEAD matches the base.
  • Adds exhaustive unit coverage, stale-branch fixtures, ADR-003 documentation, roadmap updates, and regenerates the workflow artefact.

Walkthrough

The ODW workflow now detects deterministic addendum branch collisions, reclaims merged branches with clean worktrees, and fails closed for unsafe states. A pure disposition helper, tests, fixtures, ADR, roadmap note, and mirrored JavaScript workflow support the change.

Changes

Worktree collision handling

Layer / File(s) Summary
Collision policy and coverage
src/workflows/df12-build-odw/worktree-collision.ts, tests/modules/worktree-collision.test.ts, tests/fixtures/recovery-repo.mjs
Define deterministic create, reclaim, and fail outcomes, test their reasons, and model stale addendum branches and worktrees.
TypeScript provisioning flow
src/workflows/df12-build-odw/exec.ts, src/workflows/df12-build-odw/main.ts, docs/adr-003-reclaim-stale-addendum-worktree-branches.md, docs/roadmap.md
Route worktree creation through collision inspection and safe reclamation, validate the resulting HEAD, standardize execution failure details, and document the workflow scope.
JavaScript workflow parity
workflows/df12-build-odw.js
Apply the same collision policy, provisioning behaviour, HEAD validation, and failure-detail formatting to the built workflow.

Sequence Diagram(s)

sequenceDiagram
  participant ODWWorkflow
  participant GitRepository
  participant Worktree
  ODWWorkflow->>GitRepository: Inspect deterministic branch and merge status
  GitRepository-->>ODWWorkflow: Return collision facts
  ODWWorkflow->>Worktree: Create or reclaim the worktree
  Worktree-->>ODWWorkflow: Return resolved HEAD
  ODWWorkflow->>GitRepository: Validate HEAD against origin/BASE
Loading

Possibly related issues

  • Link #33: Both changes address stale deterministic ODW addendum branches during worktree creation.

Suggested labels: Issue

Suggested reviewers: leynos

Poem

Reclaim the branch when it’s merged and clean,
Fail closed where unsafe states are seen.
Fetch the base, reset the tree,
Let deterministic paths run free.
Tests guard each choice in flight.


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (3 errors, 6 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Testing (Overall) ❌ Error Fail: the pure helper is covered, but the actual stale-branch reclaim path in createWorktree has no integration test, and the new fixture option is unused. Add an end-to-end test that uses a stale addendum repo state to assert createWorktree reclaims or fails closed correctly, and cover execFailureDetail/error notes too.
Module-Level Documentation ❌ Error Only main.ts has a /** @file … */ docblock; the touched modules mostly start with plain // comments or //@. Add an /** @file … */ docblock to each touched module, or exclude modules outside the repo’s docstring gate.
Unit Architecture ❌ Error main.ts still hard-codes git/process calls and hides roadmap-read failure inside inspectWorktreeCollision; only the pure helper is seam-tested. Extract injected git/roadmap readers for collision provisioning, surface inspection read failures explicitly, and add tests at the command seam.
Docstring Coverage ⚠️ Warning Docstring coverage is 31.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
User-Facing Documentation ⚠️ Warning docs/users-guide.md covers addendum selection and recovery, but not the new stale-addendum collision reclaim behaviour; only ADR/roadmap mention it. Add a user-guide section near addendum selection explaining stale addendum branch collisions, safe reclaim conditions, and the new non-fatal path.
Developer Documentation ⚠️ Warning src adds new internal APIs/module (execFailureDetail, worktree-collision.ts), but docs/developers-guide.md was untouched and still omits them. Update the developer guide’s module list/boundaries to cover worktree-collision.ts, decideWorktreeDisposition, and execFailureDetail, and note the collision-reclaim policy.
Testing (Unit And Behavioural) ⚠️ Warning Only the pure decideWorktreeDisposition helper is unit-tested; no behavioural or end-to-end test drives createWorktree’s stale-branch reclaim path. Add a behavioural smoke/integration test that exercises the real createWorktree/workflow boundary with a stale addendum branch, and keep the helper unit tests as support.
Testing (Property / Proof) ⚠️ Warning The new pure 5-boolean disposition helper adds a fail-closed invariant, but the tests are hand-picked examples only; no property-based sweep or proof appears. Add fast-check properties (or a small exhaustive proof/table) for the full collision state space, especially branchExists/merged/worktreeDirty/corroboration interactions.
Observability ⚠️ Warning Add structured logs at branch-exists, reclaim, and fail boundaries; createWorktree only returns notes and run-task logs one generic success line. Emit a bounded collision metric or structured event plus decision logs (create/reclaim/fail, branch, worktree, reason) so operators can diagnose the new path.
Concurrency And State ❓ Inconclusive placeholder Need code evidence before verdict.
✅ Passed checks (10 passed)
Check name Status Explanation
Title check ✅ Passed Align the title with #42 and the stale addendum worktree collision fix.
Description check ✅ Passed Keep the description aligned with the stale addendum branch collision fix.
Linked Issues check ✅ Passed Satisfy #42 by reclaiming stale merged branches and avoiding git worktree add -b halts.
Out of Scope Changes check ✅ Passed Keep the added docs, helper, tests, fixture, and artefact within the collision-reclaim scope.
Testing (Compile-Time / Ui) ✅ Passed The PR adds runtime logic only; the new decision helper is covered by focused unit tests, and the exact reason strings are asserted directly rather than via brittle snapshots.
Domain Architecture ✅ Passed PASS: isolate the collision policy in a pure helper; keep git/path/exec work in main.ts, and test the policy without infrastructure.
Security And Privacy ✅ Passed PASS: Verify branch names are sanitised by roadmapIdSlug, git calls use arg arrays not shells, and only fake test creds appear.
Performance And Resource Use ✅ Passed Accept it: the new git worktree path is O(1) per collision, with no unbounded loops or memory growth.
Architectural Complexity And Maintainability ✅ Passed Pure collision helper and exec error formatter are small, local seams that reduce main.ts complexity; no extra layer, registry, or cycle was introduced.
Rust Compiler Lint Integrity ✅ Passed PASS: the PR only changes .md/.ts/.mjs/.js files; no Rust code, lint suppressions, or clone-related edits are present.
📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #42

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-42-addendum-worktree-collision

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Biome (2.5.3)
workflows/df12-build-odw.js

File contains syntax errors that prevent linting: Line 3954: Illegal return statement outside of a function


Comment @coderabbitai help to get the list of available commands.

leynos added 2 commits July 17, 2026 23:36
`createWorktree` derives a deterministic branch name
(`roadmap-<id>[-addendum]`, no unique suffix) and runs `git worktree add
-b <branch>` with no pre-check. When a completed addendum round from a
prior run leaves its branch behind (worktree hoovered, branch never
deleted), the next selection of that task re-derives the same name, the
add throws, and `runTask` turns it into a `failed`/`worktree` halt. A
harmless, fully-merged leftover therefore blocks real addendum work
(issue #42).

Detect the collision inside `createWorktree` and decide its fate through
a pure, unit-tested helper (`decideWorktreeDisposition`) kept out of the
verified recovery-decision twin. The rule is reclaim-when-safe,
fail-closed otherwise: reclaim only when the branch tip is a merged
ancestor of `origin/BASE` and any worktree on it is clean; fail closed
(preserving today's halt) on unmerged commits or a dirty worktree.
`candidateRoadmapComplete` only corroborates a reclaim and never
licenses discarding work.

Reclaim uses sandbox-permitted git commands only (`worktree prune`,
`branch -f`, plain `worktree add`, `reset --hard origin/BASE`), then
re-runs the existing HEAD-versus-base verification. The roadmap read is
lazy, taken only on the rare collision path.

Record the scoped destructive-operation policy in ADR 003 and against
roadmap item 4.2.1: on-demand reclaim of one deterministic name is an
exception; the general `discard`-branch sweeper stays deferred.

Cover the decision table exhaustively with a pure unit test and model
the real-git leftover state with a `withStaleAddendumBranch` fixture
option. Regenerate the committed artefact.
The collision-detection and reclaim wiring added for issue #42 pushed
createWorktree over the CodeScene Complex Method threshold (cc 11).
Extract the fresh-create-or-reclaim disposition into
provisionWorktreeForBranch so createWorktree stays a straight-line
orchestration that delegates, and pass the branch and worktree path as a
WorktreeTarget object rather than two loose primitives to avoid tripping
the module's Primitive Obsession rule.

Extract the duplicated exec-failure formatting shared by createWorktree
and readRoadmapForSelection into execFailureDetail in exec.ts, its
natural home alongside execFileText which throws these errors. This
removes the last of createWorktree's excess complexity and de-duplicates
the detail-flattening logic.

Behaviour is unchanged: the reclaim-when-safe and fail-closed decisions,
the HEAD verification, and every returned note are identical. The file's
code-health score rises from 5.58 to 5.74 and createWorktree no longer
trips any smell.
@leynos
leynos force-pushed the issue-42-addendum-worktree-collision branch from 5bdb862 to e7cbb88 Compare July 17, 2026 22:36
@leynos
leynos marked this pull request as ready for review July 22, 2026 22:25

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @leynos, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e7cbb88d4c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

reason: 'pre-existing branch carries commits not merged into the base; refusing to discard unmerged work',
}
}
if (facts.worktreeExists && facts.worktreeDirty) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Refuse to reclaim a branch held by a live worktree

When another workflow run has just provisioned this deterministic branch, its registered worktree is initially clean and its tip is still an ancestor of origin/BASE, so it passes this check and falls through to reclaim. reclaimStaleWorktree then resets and returns that same path, allowing both runs to dispatch agents into one worktree; subsequent edits, commits, and integration can race or be discarded by the reset. A clean registered worktree is not proof that it is stale, so reclamation needs an ownership/liveness check or must refuse branches that still have a live worktree.

Useful? React with 👍 / 👎.

@pandalump

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot added the Issue label Jul 23, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/workflows/df12-build-odw/main.ts`:
- Around line 375-438: Add the established exec timeout option to every new git
invocation in worktreeBranchExists, inspectWorktreeCollision, and
reclaimStaleWorktree, including merge-base, worktree list, status, prune, reset,
branch, and worktree add. Also add a higher-level timeout guard around
createWorktree so the workflow cannot wait indefinitely when git hangs, reusing
the existing timeout configuration rather than introducing unrelated changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 48f4cdb2-1938-4a7f-9b3f-cafc3ec4becb

📥 Commits

Reviewing files that changed from the base of the PR and between 6327f14 and e7cbb88.

📒 Files selected for processing (8)
  • docs/adr-003-reclaim-stale-addendum-worktree-branches.md
  • docs/roadmap.md
  • src/workflows/df12-build-odw/exec.ts
  • src/workflows/df12-build-odw/main.ts
  • src/workflows/df12-build-odw/worktree-collision.ts
  • tests/fixtures/recovery-repo.mjs
  • tests/modules/worktree-collision.test.ts
  • workflows/df12-build-odw.js
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • leynos/nixie (auto-detected)

Comment on lines +375 to +438
async function worktreeBranchExists(branch: string): Promise<boolean> {
const probe = await execFileStatus('git', ['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`])
return probe.ok
}

// Read the git facts a collision disposition is decided from. Only invoked on
// the rare path where the deterministic branch already exists, so the roadmap
// read (a corroborating signal only) stays off the common no-collision path.
async function inspectWorktreeCollision(branch: string): Promise<WorktreeCollision> {
const merged = await execFileStatus('git', ['merge-base', '--is-ancestor', branch, `origin/${BASE}`])
const list = await execFileStatus('git', ['worktree', 'list', '--porcelain'])
const entry = list.ok ? parseWorktreeList(list.stdout).find((candidate) => candidate.branch === branch) : undefined
const existingWorktreePath = entry?.worktreePath || ''
let worktreeDirty = false
if (existingWorktreePath) {
const status = await execFileStatus('git', ['-C', existingWorktreePath, 'status', '--porcelain'])
// A worktree the host cannot probe is treated as dirty: fail closed rather
// than reset a tree whose cleanliness is unverifiable.
worktreeDirty = !status.ok || status.stdout.trim() !== ''
}
let candidateComplete = false
const parsed = branchToRoadmapId(branch)
if (parsed) {
try {
const roadmap = await readRoadmapForSelection()
const roadmapTask = roadmapTaskIndex(roadmap.text).get(parsed.id)
if (roadmapTask) candidateComplete = candidateRoadmapComplete(roadmapTask, parsed.isAddendum)
} catch {
// Roadmap completeness only corroborates a reclaim; a read failure must
// never upgrade a fail to a reclaim, so it simply stays false here.
}
}
return {
facts: {
branchExists: true,
branchMergedIntoBase: merged.ok,
worktreeExists: Boolean(existingWorktreePath),
worktreeDirty,
candidateRoadmapComplete: candidateComplete,
},
existingWorktreePath,
}
}

// Reclaim a stale, fully-merged branch onto origin/BASE using only
// sandbox-permitted git commands (see the supervisor skill's environment
// safety-net constraints — no `worktree remove --force`, no `branch -D`):
// reset an existing clean worktree in place, or force the orphaned branch to
// the base and attach a fresh worktree at the deterministic path. Returns the
// reclaimed worktree path so the caller can verify its HEAD.
async function reclaimStaleWorktree(
branch: string,
worktreePath: string,
existingWorktreePath: string,
): Promise<string> {
await execFileStatus('git', ['worktree', 'prune'])
if (existingWorktreePath) {
await execFileText('git', ['-C', existingWorktreePath, 'reset', '--hard', `origin/${BASE}`])
return existingWorktreePath
}
await execFileText('git', ['branch', '-f', branch, `origin/${BASE}`])
await execFileText('git', ['worktree', 'add', worktreePath, branch])
return worktreePath
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'timeoutMs' src/workflows/df12-build-odw

Repository: leynos/df12-build

Length of output: 681


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- exec.ts ---'
cat -n src/workflows/df12-build-odw/exec.ts

echo
echo '--- timeout callsites in main.ts ---'
rg -n 'timeoutMs|execFile(Status|Text)|createWorktree|worktreeBranchExists|inspectWorktreeCollision|reclaimStaleWorktree' src/workflows/df12-build-odw/main.ts

Repository: leynos/df12-build

Length of output: 7155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- execFileText/execFileStatus timeout usages ---'
rg -n "execFile(Text|Status)\([^)]*timeoutMs|timeoutMs\s*:" src/workflows/df12-build-odw

echo
echo '--- createWorktree and nearby control flow ---'
sed -n '477,530p' src/workflows/df12-build-odw/main.ts

echo
echo '--- top-level loop / orchestration ---'
sed -n '880,950p' src/workflows/df12-build-odw/main.ts

Repository: leynos/df12-build

Length of output: 206


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- createWorktree and nearby control flow ---'
sed -n '477,530p' src/workflows/df12-build-odw/main.ts

echo
echo '--- top-level orchestration ---'
sed -n '880,950p' src/workflows/df12-build-odw/main.ts

Repository: leynos/df12-build

Length of output: 3927


Add a timeout to the new git calls. exec.ts already supports timeoutMs, but these merge-base, worktree list, status, worktree prune, reset --hard, branch -f, and worktree add calls do not use it, and createWorktree has no higher-level guard. A hung git process can stall the workflow indefinitely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/workflows/df12-build-odw/main.ts` around lines 375 - 438, Add the
established exec timeout option to every new git invocation in
worktreeBranchExists, inspectWorktreeCollision, and reclaimStaleWorktree,
including merge-base, worktree list, status, prune, reset, branch, and worktree
add. Also add a higher-level timeout guard around createWorktree so the workflow
cannot wait indefinitely when git hangs, reusing the existing timeout
configuration rather than introducing unrelated changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ODW addendum worktree creation halts on stale completed branch

1 participant