Skip to content

feat(board): salvage a stranded card's worktree without a coder (#427) - #439

Merged
mabry1985 merged 16 commits into
mainfrom
feat/salvage-427
Sep 11, 2026
Merged

mabry1985 merged 16 commits into
mainfrom
feat/salvage-427

Conversation

@mabry1985

@mabry1985 mabry1985 commented Sep 10, 2026

Copy link
Copy Markdown
Member

Fixes #427.

Stacked on #434 (base fix/publish-reap-400-405). The salvage relies on three things #434 adds:

  • the stage_all fix, so a salvage commit can't carry the board's node_modules link;
  • promote_worktree's save-before-clear;
  • the tree enumeration and per-path tree lock that feature_worktrees and hold_trees sit on.

Merge #434 first. ⚠ Retarget this PR to main before #434's branch is deleted: deleting a stacked PR's base branch can close the child instead of retargeting it.

What was missing

When a card's worktree already held finished work, the only way to publish it was to dispatch a coder, because every publication step (gate, commit, push, PR) sat behind a dispatch. On 2026-09-07 bd-ezs7's recovery stalled exactly there. The finished 170 lines were stranded in feat-bd-ezs7.g1, and the coder delegate was down for an unrelated reason: protoCLI under Node v26 failed every model call.

What this adds

board_salvage_feature(feature_id, force=False, tree="") and POST /features/{fid}/salvage ({force?, tree?}). They run the board's own publishing steps with no coder in the path. The crash salvage (_salvage_verified_candidate) now shares its two halves: _gate_tree (fixups + gate) and _open_tree_pr.

It is an operator override: it skips the drive's goal, requirement-ledger and source-issue checks. CI still applies, and so does the review gate when it is on.

  1. Refuses, changing nothing:

    • while a live drive or another salvage owns the card;
    • for a card that isn't stranded. Only in_progress with no drive, or blocked, qualifies; a ready card could be claimed mid-publish, so the operator blocks it first;
    • when no worktree of the card has changes vs base, or several do and tree doesn't pick one.
  2. Every tree is proven first. worktree.own_worktree checks that the tree is its own top level and that the repo registered it. A leftover with no .git, a husk, or a separate clone under the card's tree name is left untouched and named in the refusal.

  3. The gate runs where the tree stands. format_cmd fixups run first, as in a drive, so they may rewrite files even when the gate then refuses. A red gate publishes nothing and returns gate-red with the output's tail.

  4. force=true on a red gate:

    • a new PR opens as a draft carrying the output;
    • an existing PR is converted (gh pr ready --undo) and the output is posted on it as a comment;
    • draft in the record is read back from GitHub. A refused conversion is reported as "NOT a draft";
    • in a repo without drafts nothing is opened, and the error names the branch already pushed, plus the gh pr create command.

    The output is fenced with more backticks than it contains.

  5. Publish, then review. A candidate tree is promoted to the card's canonical branch, then committed, pushed and opened (or pushed onto the card's PR). Under the claim lock, the card goes to in_review:

    • a card parked in_progress goes through open_review;
    • a card blocked out of in_review (CI-fix rounds spent, or a review-gate block) goes straight back there, on its own PR.

    With review_gate on, the salvage runs the gate inline, as a drive does. The reconcile only picks up review-pending when merge_poll is on.

  6. A cancel mid-publish stops it before the PR, or closes the PR it has just opened (cancelled, 409). The salvage holds its trees (hold_trees, reentrant for its own promotion), so the cancel's reap waits, then saves and removes the tree as usual.

  7. The reservation. The card is reserved in _inflight_files, the same claim a drive holds, and only the salvage that made it releases it (identity check). The claim scan (reserved, not counted as a livelock), boot recovery, the orphan sweep, the blocked sweep and the worktree sweep all skip a reserved card. feat(board): attach an externally opened PR to the card it belongs to (#402) #437's claim-guarded re-reads treat a card reserved during their gh round-trip as owned.

HTTP status by outcome: published 200 · gate-red / refused / cancelled 409 · not-found 404 · loop-not-running 503 · error 502. The record carries branch. The route is on the bearer-gated operator surface. It and the tool reach the running loop through request_salvage, as board_dispatch does.

docs/lifecycle.md also states the hung-drive limitation. The salvage refuses while a drive owns the card, and no verb stops a drive on its own. Cancelling the card or restarting the host saves the tree to a stranded/… branch and removes it, so that branch then has to be published by hand.

Review round

An adversarial review found seven defects. All are fixed, each with a test.

# Finding Fix
6 A leftover feat-… dir with no .git let git answer for the main checkout: the salvage counted the operator's own commit as the card's work and committed onto the operator's branch own_worktree guard on every tree; commits_ahead refuses non-trees
1 force on a card with an open PR adopted the READY PR and reported "draft" convert + comment + read back; fake gh models "already exists"
2 A card blocked out of in_review failed open_review's precondition after its block was cleared returns to in_review on its PR
3 review_gate on + merge_poll off: nothing ever reviewed a salvaged PR inline review gate
4 A cancel during the publish still opened a PR, and its reap could remove the tree mid-gate cancel checked before and after the PR; trees held
5 The claim scan and boot recovery ignored the reservation, and the salvage released another holder's claim both skip; release by identity
Fence escape; repos without drafts; docs gaps fixed / reported truthfully / documented

#437 overlaps, resolved in the merge: #437's _tools() docs guard verbatim; #437's board_dispatch row (⚠️, after board_deliver), so this PR's duplicate is dropped; __all__ exports both attach_external_pr and request_salvage; the worktree seam contract is 30 REAL / 3 EXEMPT / 0 UNCOVERED; board_salvage_feature joins the async-aware never-raise sweep.

Tests

tests/test_salvage_427.py has 27 tests:

  • real git: a bare origin + clone, with a real commit and push;
  • a real br board, so every card transition is the store's own;
  • only _gh faked. It models pr create "already exists", repos without drafts, pr ready --undo, pr close, pr view state/isDraft, and the comments API.

Red-check (the final test file run against pristine git archive exports):

Mutation-checked on the fixed code. Reverting each guard fails its test:

  • inline review gate
  • hold_trees
  • the pre-PR and post-PR cancel checks
  • --undo
  • draft read-back
  • own_worktree filter ("committed into another repo")
  • fence width
  • blocked-sweep skip
  • the three reservation-window re-reads

Gates: ruff format --check . ✓ · ruff check . ✓ · pytest -q → 2201 passed, 16 skipped (exit 0), on main through #433.

Later merges (main #431, #438, #436 and #433, via #434):

Residual risk

  • In a repo without draft PRs, force onto an existing PR leaves it ready. The record, the card comment and the PR comment all say so, and CI plus the review gate still stand between it and a merge.
  • The real-br tests run on br 0.2.16 locally. CI's full-suite leg uses br 0.1.23.

🤖 Generated with Claude Code

https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av

mabry1985 and others added 3 commits September 10, 2026 15:07
…else (#405)

A coder that dies before its candidate is promoted leaves the only copy of its
work in that tree, and every automatic edge that touched it was destructive:
a re-dispatch ran create_worktree, whose "clean a prior run's leftovers" step
is `git worktree remove --force` + `git branch -D`; promote_worktree cleared a
stale canonical the same way; the by-id reap (merge, closed PR, cancel, done,
health sweep) and the shutdown reap removed whatever was there. bd-ezs7's
finished 170 lines (#400) survived only because its drive had hung in #423's
unbounded proc.wait() and still held the card's file claim, so nothing
re-dispatched it.

Now "work" is every uncommitted change git reports (modified, staged, deleted,
untracked), minus the board's own droppings (.proto/.cursor scratch and the
node_modules links it adds), plus, on a candidate branch, commits no other
ref holds. A tree holding work is never destroyed automatically:

- a fresh build discards the drive's OWN previous attempt explicitly, then
  checks every tree the card has; if any holds work the card blocks under a
  new non-healing `stranded-work` class whose reason names each path, its
  branch, what is in it, and how to recover or discard it. No coder runs.
- create_worktree / promote_worktree raise StrandedWorkError instead of
  force-cleaning such a tree.
- reap_feature_worktree keeps it and logs it once; clean trees still reap.
- shutdown keeps an interrupted tree holding work; its card blocks on the
  next dispatch.

Max-Mode's losers are the drive's own judged candidates, so they are now
discarded by path (like the solve ladder's), not through the guarded by-id
reap. unpublished_work is a new local-git seam, REAL-covered against real git.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
… saving fails

Blocking every card with stranded work turned each plugin roll into manual toil:
a restart mid-build strands one tree per in-flight drive, and the board cannot
tell a half-written tree from a finished one waiting on its gate. So instead of
refusing, every edge that would remove such a tree now SAVES it first:
preserve_worktree commits the tree's HEAD plus its working state (through
stage_all, into a private GIT_INDEX_FILE, so the tree, its index and its branch
are untouched; commit-tree so no hooks or signing prompt run; identity pinned)
onto a NEW branch stranded/<tree id>/<UTC stamp>. update-ref with an empty old
value never overwrites an existing branch, and the ref's tree is read back and
compared before the tree may go.

- a fresh build saves every stranded tree the card owns, removes them, leaves a
  card comment (branch, diffstat, `git diff origin/<base>...`, cherry-pick) and
  builds on; only a tree whose save FAILED blocks the card as stranded-work,
  kept intact.
- shutdown saves the interrupted tree, comments, and reaps it; the next boot
  rebuilds as before.
- the by-id reap and create_worktree/promote_worktree save then clear; the
  latter two refuse with StrandedWorkError only when saving fails.

stage_all now also leaves out the node_modules symlink create_worktree links
in: a `node_modules/` ignore pattern matches only a real directory, so the
board's own link was committed into PRs. The brief no longer tells an edit-only
coder to "push the branch if you can" (#400): it now says the loop commits,
runs the pre-PR checks, pushes and opens the PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
mabry1985 added a commit that referenced this pull request Sep 10, 2026
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
mabry1985 and others added 7 commits September 10, 2026 16:30
Adversarial review of #434 showed "every edge that would remove a tree holding
unpublished work saves it first" did not hold yet. Each gap is now closed, with
a real-git test:

1. Committed-but-unpushed work on the CARD's branch (the verified candidate
   coder_seam commits before open_pr pushes it) was not counted, so a restart
   or a failed push lost it. Every branch's commits count now - HEAD and the
   branch, no other ref holding them - unless their CONTENT is already
   published: `git merge-tree --write-tree` into origin/<branch>, origin/<base>
   or origin/HEAD changes nothing. That keeps rebased and squash-merged history
   from reading as stranded, and auto-merge now reaps the tree before deleting
   the PR branch, while that tracking ref still proves it.
2. Cancel removed the tree unsaved, racing the cancel route's save-first reap.
   Every removal now goes through one locked save-then-remove
   (discard_worktree), so two edges on one tree give one save and one removal.
3. Our own naming made saves fail: the ref dropped the title slug and stamped
   to the second. It is now stranded/<tree dir>/<UTC ms>, retried with a
   suffix on a taken name, never an overwrite.
4. A saved tree git would not remove was reported removed, stayed, and was
   re-saved (or blocked) on every edge; a husk was kept forever. After a
   verified save the tree is deleted anyway (never a locked one), reports say
   truthfully whether it went, a husk is moved aside with its bytes and its
   branch's commits saved, and the block's discard command works on a husk.
5. Commits on a detached HEAD were lost; HEAD is counted and saved, with the
   branch tip as a second parent when HEAD moved off it.
6. A fresh build swept the operator's live test-rung tree; card sweeps now
   leave feat-<id>.test* trees to the diagnostic that owns them.
7. A nested git repository was half saved (a gitlink) and its files deleted;
   such a tree is now refused and kept whole.
8. A drive's terminal block (a push GitHub refused, say) removed the finished,
   committed tree unsaved; block and cancel removals now save first.

Saves run with hooks off (core.hooksPath=/dev/null) - update-ref fires
reference-transaction - and the brief only promises pre-PR checks when a gate
is configured. The seam registry gains the three new helpers (27 REAL).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
Resolves the overlaps with #425/#432/#435/#428: failures.py keeps main's seam
shapes beside STRANDED_WORK_CLASS; the kernel exports both TOO_WIDE_CLASS and
STRANDED_WORK_CLASS; the terminal-block path takes main's structure (the #378
ask moved to the park path) with this branch's save-first discard; max-mode's
all-raised fan-out keeps #425's representative failure and discards its own
candidates by path. The new too-wide park removal saves first too, and the
#425 test now expects the candidates discarded by path, not reaped by id.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
bd-ezs7's coder finished 170 lines and died before publishing them, and
recovering that work needed a coder: every publication step was only reachable
by dispatching one, and that day the coder delegate was down for an unrelated
reason. `board_salvage_feature` / `POST /features/{fid}/salvage` runs the
board's own tail instead: commit what the tree holds, the pre-PR gate, push,
open the PR, move the card to in_review.

It refuses, touching nothing, while a live drive or another salvage owns the
card, for a card that is not stranded (in_progress with no drive, or blocked -
a ready card could be claimed mid-publish), and when no worktree - or several,
unless `tree` names one - has changes vs base. The gate runs where the tree
stands: a red gate publishes nothing and hands back the output's tail unless
force=true, which opens a DRAFT PR carrying that output. A candidate is
promoted to the card's branch first; with review_gate on the card enters
review-pending. The card is reserved in _inflight_files for the duration, so no
sweep requeues it, reaps its trees or auto-unblocks it mid-publish.

The crash salvage of a verified candidate (#91) and this edge now share the
publish tail (_gate_tree + _open_tree_pr); open_pr gains draft=. The docs guard
now also scans async tools - board_dispatch (#390) had shipped undocumented
because it only looked at plain defs; it is documented now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
…ts card (#427)

An adversarial review of the salvage found it could do damage no drive can:

- A leftover `feat-…` directory with no `.git` sits inside the main checkout, so git
  run in it answered for THAT: the salvage counted the operator's own commit as the
  card's work and committed the card's title onto the operator's branch. Every tree is
  now proven first (`own_worktree`: its own top level, and registered by the repo), and
  `commits_ahead` refuses to measure a directory that is not one.
- `force` against a card that already had a PR adopted the READY PR and reported
  "draft". The adopted PR is now converted (`gh pr ready --undo`), the gate output is
  posted on it, and `draft` is read back from GitHub rather than assumed. A repo without
  drafts gets an error naming the branch it already pushed.
- A card blocked out of in_review had its block cleared and then failed open_review's
  in_progress precondition. It now goes back to in_review on its own PR.
- With review_gate on and merge_poll off nothing ever ran the review gate on a salvaged
  PR; the salvage now runs it inline, as a drive does.
- A cancel during the publish still opened a PR for a cancelled card, and its reap could
  remove the tree mid-gate. The salvage re-checks the cancel before and after opening the
  PR (closing one it just opened), and holds its trees so the reap waits.
- The claim scan and boot recovery ignored the salvage's reservation, and the salvage
  released the reservation unconditionally. Both now skip a reserved card, and the
  salvage releases only its own (by identity).

The gate output is fenced with more backticks than it contains. Docs state the salvage
is an operator override (goal/ledger/source-issue checks skipped; CI and the review gate
still apply), that format_cmd may rewrite the tree even on a refusal, and how a hung
drive has to be handled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
Brings in #430 and #437. The one conflict was the worktree seam contract: main's #402
made `pr_identity` REAL (24) while this branch added #405's four stranded-work seams
(27), so the contract is now 28 REAL / 3 EXEMPT / 0 UNCOVERED, and the README says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
…salvage-427

Resolutions, taking #437's text where both branches did the same job:
- tests/test_docs_reference.py: #437's async-aware `_tools()`, verbatim.
- docs/tools.md: #437's `board_dispatch` row (with its ⚠️), in its place after
  `board_deliver`; the salvage PR's own row for it is dropped.
- loop/__init__.py `__all__`: `attach_external_pr` and `request_salvage` both exported.
- api.py / docs/api.md: the attach-pr and salvage routes side by side.
- Worktree seam contract: 30 REAL (main's 24 with `pr_identity`, #405's four, and #427's
  `commits_ahead` and `own_worktree`), in the test and the README.
- tests/test_board_tools.py: #437 extended the never-raise sweep to async tools, so
  `board_salvage_feature` joins `_NO_STORE_ARGS` (its store reads run inside the loop).

#437 added claim-guarded re-reads before the orphan reconcile and the blocked sweep move
a card, because an attach can land during their gh round-trip. A salvage can start in
the same window. Both re-reads now treat a reserved card as owned, and the orphan
reconcile stops after its gh read if the card was reserved meanwhile, so a crash salvage
never runs on a tree an operator salvage is publishing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
mabry1985 and others added 5 commits September 10, 2026 17:40
Brings in #431, #438 and #436. #436 overlaps this branch semantically: it re-reads the
card before every drive-side block, retry, publish and hand-off, and a drive whose card
was moved on under it stands aside. It keeps the worktree, and a held or done card gets
no PR, so the finished work stays unpushed in that tree. This branch saves any tree
holding work before removing it. The combined rule:

- Every block site takes #436's gate (`_block_or_stand_aside`), and the removal after a
  real block is this branch's save-then-remove (`_discard_tree`), never a bare
  `remove_worktree`. The six conflicting sites are resolved that way.
- The stranded-work block (a fresh build that could not save a leftover tree) now goes
  through `_block_or_stand_aside` too, so it never overwrites a card someone moved.
- A build that stood aside has let go of its tree (`_stand_aside` drops it from
  `_inflight`), so shutdown leaves it alone. The by-id reap and the card's next fresh
  build save its work to a `stranded/…` branch before removing it.

tests/test_moved_build_tree_405.py runs that case end to end on real git and a real
`br` board: a human holds the card while its coder finishes, and the build stands aside
with no PR. Shutdown then leaves the tree, and the reap saves it to `stranded/…`. The
card keeps the human's state, reason and class, with no PR. A second test lifts the
hold, and the next round's clean-up saves the first round's work before building fresh.
Both tests fail on main (nothing is saved) and on this branch's previous head (the held
card gets a PR).

docs/lifecycle.md keeps both new sections, cross-referenced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
…pt away (#398, #405)

A retry that rebuilds from scratch removes the drive's own failed attempt without saving
it. That is by design (#405): the drive built that tree and judged it. But #436's
ownership check sits at the top of the attempt, and the prompt is built after it,
including a knowledge-graph fetch that can take seconds. A human hold that landed in
that window turned the tree into a moved build's, and it was removed unsaved anyway. A
coder that fell over half-way through leaves its partial work there.

The drive now re-reads the card right before that removal. A card moved on keeps its
tree through the same stand-aside as every other moved build, and whatever ends that
tree later saves it first. The test fails without the re-read: the retry is dispatched
on the held card and attempt 1's tree is gone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
… feat/salvage-427

No textual conflicts. The main changes meet the salvage in these places:

- #436's requeue refusal reads `_inflight_files`, so a card under a salvage cannot be
  requeued out from under it, and #437's attach refuses it the same way.
- A build that stands aside from a held card (#398) leaves exactly what the salvage
  publishes: the card blocked, and the finished work unpushed in its tree. A new test in
  tests/test_moved_build_tree_405.py runs that end to end on real git and a real `br`
  board. A human holds the card mid-build, and the build stands aside with no PR. The
  salvage then publishes the tree as it stands, on the card's own branch, and puts the
  card in review. Nothing is set aside.
- docs/lifecycle.md says so in the salvage section.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
No conflicts. #433 moves the working-state snapshot onto its own refresher task, which
`stop()` now cancels alongside the tick. Shutdown's save-then-remove of in-flight trees
still runs after both. `clear_blocked` now drops any blocked-class label, and that
includes `stranded-work`, so unblocking a card after recovering its tree works as the
docs say.

The seam ratchet, recomputed from the scanner: worktree.py has 31 seams (28 REAL,
3 EXEMPT, 0 UNCOVERED) and store.py has 49 (33 REAL, 16 UNCOVERED). The constants are
exact: MAX_UNCOVERED_WORKTREE 0, MAX_EXEMPT_WORKTREE 3, MAX_UNCOVERED_STORE 16.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
The one conflict was module constants in loop/reconcile.py: #433's refresher poll interval
and the salvage's gate-output constants. Both are kept.

#431 bounds a stalled `br` call by raising. A salvage whose first board read raised used
to escape as an exception, which the route turned into a 500. It now returns an `error`
record and still releases the card's reservation. The failures after the push already
land in the record. The new test fails without this.

The seam ratchet, recomputed from the scanner: worktree.py has 33 seams (30 REAL,
3 EXEMPT, 0 UNCOVERED) and store.py has 49 (33 REAL, 16 UNCOVERED, which is exactly
MAX_UNCOVERED_STORE).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
Base automatically changed from fix/publish-reap-400-405 to main September 11, 2026 01:21
main since the last merge carries only #434's squash (ca9498f), whose tree is
identical to #434's branch tip, which this branch already contains; conflicts
resolved to this branch's side, and the resulting tree equals df54273's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2V6GRejF7mNukAoYjj2Av
@mabry1985
mabry1985 merged commit 8dc872b into main Sep 11, 2026
4 checks passed
@mabry1985
mabry1985 deleted the feat/salvage-427 branch September 11, 2026 01:25

@protoreview protoreview 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.

QA panel review — PASS

code-review-structural · head df542732f6ca · formal

⚠️ PR advanced 2 commit(s) during this round (df542732f6ca62f12192563a); 0 finding(s) in the delta were demoted to possibly addressed.

All four review angles (correctness, removed-behavior, conventions, and the partial structural pass) returned zero findings, and the verifier confirmed the empty array stands as-is. Overall risk is low; there is nothing to fix first. The panel did not disagree on any point. The one coverage gap worth noting: the structural pass was only partial, so dependency-graph and cross-module concerns may not be fully exercised — if the diff touches shared interfaces or public APIs, a targeted follow-up on those seams is advisable.

No findings — the review came back clean.

findings JSON (machine-readable)
[]

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(board): salvage an existing worktree without a coder — gate, commit, push and PR it for a stranded card

1 participant