Skip to content

fix(teardown): recognize work that landed under rewritten commits - #2586

Open
IanQiu979 wants to merge 23 commits into
kunchenguid:mainfrom
IanQiu979:fm/fm-teardown-false-refusal-after-rebase
Open

fix(teardown): recognize work that landed under rewritten commits#2586
IanQiu979 wants to merge 23 commits into
kunchenguid:mainfrom
IanQiu979:fm/fm-teardown-false-refusal-after-rebase

Conversation

@IanQiu979

Copy link
Copy Markdown

Intent

Goal: teach bin/fm-teardown.sh's landed-work test about patch equivalence, so it stops
falsely refusing to reclaim a worktree whose content landed under REWRITTEN commits,
without weakening its refusal of genuinely unlanded work.

Background / the defect: the landed-work test asked only whether a branch's COMMITS are
reachable from the default branch. A rebase gives every commit a new object id, so after
any rebase the pre-rebase branch can never satisfy that test even when its content landed
in full. This is not a corner case here: bin/fm-merge-local.sh is fast-forward-only, so any
second chain landing into a moved default branch must rebase first. Every multi-chain
local-only graph therefore ended with parent nodes that ordinary teardown could never
reclaim, and the remedies the refusal offered were both wrong - merging again is redundant,
and --force needs captain authority for work that is not actually at risk.

The fix: a new patches_are_in_ref() asks whether every commit HEAD holds that the reference
cannot reach is already present there as a PATCH - the relationship git cherry reports.
work_is_landed() consults it only after pr_is_merged and the content check have already
failed, and the local-only arm consults it only after the reachability listing already
decided to refuse.

Deliberate decisions:

  1. Patch equivalence is purely ADDITIVE. It is reached only after the existing proofs have
    failed, so it can only ever turn a refusal into an allow on proof, never turn an allow
    into a refusal, and never relax an existing guarantee.
  2. The patch-id comparison is hand-rolled rather than shelling out to git cherry. Verified
    empirically on git 2.50.1: git cherry OMITS merge commits from its output entirely. A
    branch whose only unlanded content lived in a merge commit's conflict resolution would
    have reported as fully landed - a real safety hole. Hand-rolling lets an unreadable or
    empty patch id count as NOT landed.
  3. Strict by construction: EVERY unreachable commit must match; an empty or unreadable patch
    (an empty commit, or a merge commit whose conflict resolution git show does not emit)
    never counts as landed; and a reference contributing no patch ids at all is inconclusive
    rather than permissive. Anything short of a complete match refuses.
  4. The uncommitted-changes check is untouched and still precedes and ANDs into every refusal
    path, so a dirty worktree refuses regardless of patch state.
  5. --force gating is unchanged. The point of the fix is to remove the NEED for --force in
    this case, not to make --force easier to reach.
  6. The local-only arm recomputes its own full commit list rather than reusing the head -5
    display list, so display truncation can never manufacture a false "landed".
    Behaviour-preserving refactor along the way: content_in_default() split into
    default_landing_ref() + content_in_ref() so both fallbacks share one ref resolution and one
    network fetch.

Tests: four cases added to tests/fm-teardown.test.sh, exercised through the executable
interface. (q1) patch landed under a rewritten commit -> ALLOW, with a vacuity guard that
fails the test if the branch turns out to be plainly reachable. (q2) one commit's patch
absent -> REFUSE. (q3) every patch landed but the worktree dirty -> REFUSE. (q4) no-mistakes
mode, patch landed on origin main under a rewritten commit and the file edited again
afterwards, so the content check is inconclusive and patch equivalence is isolated as the
thing doing the work -> ALLOW. Proven genuinely distinguishing by mutation: relaxing the
all-commits-must-match rule to any-match breaks q2; dropping dirty enforcement breaks q3.
Full suite 62 ok / 0 failures; the q1 case fails before the fix. Adjacent suites all pass:
teardown-endpoint-safety, gate-refuse, secondmate-safety, pr-merge, decision-hold-lifecycle,
public-followup. bin/fm-lint.sh clean.

Documentation: the script header is the single owner of the "landed" definition per the
repo's knowledge-placement rules, so it is updated in place to cover both the rewritten-
commit case and why the local-only path meets it most often. AGENTS.md already points at
bin/fm-teardown.sh as the owner of the complete landed-work test, so it needs no change.

Decisions already made by the human on the previous review round, carried forward so they
are not re-litigated:

  • The local-only arm deliberately runs the patch-equivalence scan even when the worktree is
    dirty. Gating it on a clean worktree would also suppress the "commits not yet on "
    detail line in the dirty refusal, and that line is the most valuable part of a teardown
    refusal - it is what tells the captain exactly what work is at stake before deciding
    anything. A redundant scan on a path that refuses anyway is a trivial cost; correctness
    beats speed on a destructive-action guard. Accepted as-is, do not reorder.
  • The landed-side scan being unbounded is accepted. It is a performance note, not a
    correctness problem.
  • A commit that landed and was subsequently reverted on the default branch still counting as
    landed is accepted. The work genuinely did land, the revert is a deliberate later choice,
    and the original commit remains reachable in history, so it is not a data-loss case.
  • The one change wanted from that round is removing the duplication between
    patches_are_in_ref() and unpushed_patches_are_in_pr_head(): duplicated logic in a
    safety-critical check is a real hazard, because a future fix applied to only one copy is
    the realistic failure mode.

What Changed

  • bin/fm-teardown.sh gains patches_are_in_ref(), a patch-equivalence check that asks whether every commit the reference cannot reach is already present there as a patch. work_is_landed() consults it only after pr_is_merged and the content check have failed, and the local-only arm consults it only after the reachability listing has already decided to refuse — so it can turn a refusal into an allow on proof, never the reverse. It is deliberately strict: every unreachable commit must match, an empty or unreadable patch id never counts as landed, and a reference contributing no patch ids is inconclusive. The uncommitted-changes check and --force gating are untouched, and the local-only arm recomputes its own full commit list rather than reusing the truncated display list.
  • Refactors alongside it: content_in_default() splits into default_landing_ref() + content_in_ref() so both fallbacks share one ref resolution and one fetch, and the patch comparison duplicated between the new check and unpushed_patches_are_in_pr_head() collapses into a single subject_patches_are_in_reference() helper. A dirty worktree whose commits provably landed now gets an accurate refusal message instead of the generic "not yet merged" wording.
  • Four cases added to tests/fm-teardown.test.sh through the executable interface — patch landed under a rewritten commit (allow, with a vacuity guard), one commit's patch absent (refuse), all patches landed but worktree dirty (refuse), and firstmate mode with the file edited after landing so patch equivalence is isolated (allow). Suite runs 62 ok / 0 failures; adjacent suites (pr-merge, gate-refuse, teardown-endpoint-safety, secondmate-safety) pass. Header docs updated in place as the owner of the "landed" definition, plus a one-line correction in docs/gitlab-merge-watch.md.

Risk Assessment

✅ Low: The round-2 changes are a behavior-preserving extraction of one shared patch-set comparison plus a narrowly scoped refusal-copy fix, both verified against every caller's prior exit-code semantics and covered by the updated q3 assertions.

Testing

Ran the full teardown suite (62 ok, 0 failures, four new rebase cases included) plus the four adjacent suites the shared patch-matching helper touches (pr-merge, gate-refuse, teardown-endpoint-safety, secondmate-safety) — all clean. Because passing unit tests alone do not show the defect is gone, I also built a real fixture that lands work exactly the way bin/fm-merge-local.sh does (rebase onto a moved main, then fast-forward) and ran the actual fm-teardown.sh CLI on it twice: the base-commit script printed "REFUSED: ... has work not yet merged into main" and exited 1, while the fixed script completed teardown and exited 0 — with the same transcript proving the commit really is unreachable from main yet its content is fully landed. A second transcript exercises the refusal side on the fixed script: a never-landed commit still refuses and is named, a dirty worktree whose patches all landed refuses with the new "its commits already landed in main / Commit the uncommitted changes" wording, and plainly unmerged work refuses unchanged. No UI surface is involved, so CLI transcripts are the end-user-visible artifact. Working tree left clean.

Evidence: Rebase-landed teardown: before vs after the fix (real git rebase, real CLI)

$ git -C $WT merge-base --is-ancestor HEAD main; echo $? # 1 = commit NOT reachable from main 1 $ git -C $WT diff HEAD main -- beta.txt # ...yet the content is identical (no output = beta.txt is fully landed on main) ### BEFORE the fix (bin/fm-teardown.sh at d843712) $ fm-teardown.sh task-x1 REFUSED: local-only worktree .../wt has work not yet merged into main and not on any remote. commits not yet on main: 5a5cab2 add beta Merge the branch into local main first (bin/fm-merge-local.sh after the captain approves), or push to a fork/remote, or get the captain's explicit OK to discard, then --force. [exit 1] ### AFTER the fix (bin/fm-teardown.sh at HEAD) $ fm-teardown.sh task-x1 teardown task-x1 complete (window firstmate:fm-task-x1, worktree .../wt) [exit 0]

### The situation an end user is in
$ git -C $WT log --oneline -1        # our task's commit
5a5cab2 add beta
$ git -C $WT log --oneline main -3   # main after the rebase-landing
1c0ce5c add beta
38f37f6 add alpha
f7a3d4b baseline
$ git -C $WT merge-base --is-ancestor HEAD main; echo $?   # 1 = commit NOT reachable from main
1
$ git -C $WT diff HEAD main -- beta.txt   # ...yet the content is identical
(no output = beta.txt is fully landed on main)

### BEFORE the fix (bin/fm-teardown.sh at d843712808658f26a7a3f248e632cb999864ca50)
$ fm-teardown.sh task-x1
    REFUSED: local-only worktree /var/folders/62/t6ydhgg91f3df0jm3k2r0fh80000gp/T/tmp.6p7fBAtmHE/case/wt has work not yet merged into main and not on any remote.
    commits not yet on main:
    5a5cab2 add beta
    Merge the branch into local main first (bin/fm-merge-local.sh after the captain approves), or push to a fork/remote, or get the captain's explicit OK to discard, then --force.
    [exit 1]

### AFTER the fix (bin/fm-teardown.sh at HEAD)
$ fm-teardown.sh task-x1
    teardown task-x1 complete (window firstmate:fm-task-x1, worktree /var/folders/62/t6ydhgg91f3df0jm3k2r0fh80000gp/T/tmp.6p7fBAtmHE/case/wt)
    Backlog: task-x1 just finished. Run tasks-axi done task-x1 --note "local main", then run tasks-axi ready for dependency-cleared candidates, check date gates, and dispatch only work whose blockers are gone and date is due.
    [exit 0]
Evidence: Refusals preserved on the fixed script (unlanded commit / dirty worktree / nothing landed)

### Safety case 1 - one commit landed by rebase, a second commit never landed REFUSED: local-only worktree .../wt has work not yet merged into main and not on any remote. commits not yet on main: 9beaaca add gamma (never landed) a22ba3e add beta [exit 1] ### Safety case 2 - every commit's patch landed by rebase, but the worktree is dirty REFUSED: local-only worktree .../wt has uncommitted changes; its commits already landed in main. uncommitted changes present Commit the uncommitted changes (or get the captain's explicit OK to discard, then --force). [exit 1] ### Safety case 3 - nothing landed at all (plain unmerged work) REFUSED: local-only worktree .../wt has work not yet merged into main and not on any remote. commits not yet on main: c8b3704 add beta [exit 1]

### Safety case 1 — one commit landed by rebase, a second commit never landed
$ fm-teardown.sh task-x1
    REFUSED: local-only worktree /var/folders/62/t6ydhgg91f3df0jm3k2r0fh80000gp/T/tmp.cQki8uY9TZ/partial/wt has work not yet merged into main and not on any remote.
    commits not yet on main:
    9beaaca add gamma (never landed)
    a22ba3e add beta
    Merge the branch into local main first (bin/fm-merge-local.sh after the captain approves), or push to a fork/remote, or get the captain's explicit OK to discard, then --force.
    [exit 1]

### Safety case 2 — every commit's patch landed by rebase, but the worktree is dirty
$ fm-teardown.sh task-x1
    REFUSED: local-only worktree /var/folders/62/t6ydhgg91f3df0jm3k2r0fh80000gp/T/tmp.cQki8uY9TZ/dirty/wt has uncommitted changes; its commits already landed in main.
    uncommitted changes present
    Commit the uncommitted changes (or get the captain's explicit OK to discard, then --force).
    [exit 1]

### Safety case 3 — nothing landed at all (plain unmerged work)
$ fm-teardown.sh task-x1
    REFUSED: local-only worktree /var/folders/62/t6ydhgg91f3df0jm3k2r0fh80000gp/T/tmp.cQki8uY9TZ/unlanded/wt has work not yet merged into main and not on any remote.
    commits not yet on main:
    c8b3704 add beta
    Merge the branch into local main first (bin/fm-merge-local.sh after the captain approves), or push to a fork/remote, or get the captain's explicit OK to discard, then --force.
    [exit 1]

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ⚠️ bin/fm-teardown.sh:910 - The intent's carried-forward human decision requires: "The one change wanted from that round is removing the duplication between patches_are_in_ref() and unpushed_patches_are_in_pr_head(): duplicated logic in a safety-critical check is a real hazard, because a future fix applied to only one copy is the realistic failure mode." That dedup is absent from the change. bin/fm-teardown.sh:910 (patches_are_in_ref) and bin/fm-teardown.sh:812 (unpushed_patches_are_in_pr_head) still each carry their own copy of the identical logic: a git log --format=%H <range> | while read commit; do patch_id_for_commit; done | sed '/^$/d' | sort -u collector, an [ -n "$ids" ] || return 1 guard, and a while read ... patch_id=$(patch_id_for_commit ...); [ -n "$patch_id" ] || return 1; grep -qxF all-must-match loop. Only the two rev ranges and the empty-candidate-list behavior differ. Suggested shape: one patch_ids_for_range() helper plus one all_patches_present <id-set> <commit-list> loop, with both callers supplying only their ranges.
  • ℹ️ bin/fm-teardown.sh:1237 - On the local-only path, when patches_are_in_ref() clears unmerged but the worktree is dirty, the refusal headline still reads "has work not yet merged into $DEFAULT and not on any remote" even though every commit provably landed and the only work at stake is the uncommitted change. The detail lines are correct (only "uncommitted changes present" prints), but the headline and the remedy line ("Merge the branch into local $DEFAULT first") point the captain at an action that is already done. The q3 test asserts the current text, so changing it is a deliberate product-copy decision.

🔧 Fix: dedupe patch matching, fix landed-but-dirty refusal copy
1 info still open:

  • ℹ️ bin/fm-teardown.sh:1255 - The new accurate dirty-but-landed refusal (line 1249) fires only when the patch scan cleared a previously non-empty unmerged. A local-only worktree that is dirty but whose commits are plainly REACHABLE from $DEFAULT still falls through to the generic headline at line 1255 ("has work not yet merged into $DEFAULT and not on any remote") plus the "Merge the branch into local $DEFAULT first" remedy, even though nothing is unmerged there either. That inconsistency is pre-existing and was explicitly scoped out by the round-1 instruction ("Leave the wording of every other refusal case alone"), so no change is expected here - noting it only so the remaining rough edge is on record.
✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-teardown.test.sh — 62 ok / 0 failures, including the four new cases (q1 rebase-landed allow, q2 absent patch refuse, q3 landed-but-dirty refuse, q4 no-mistakes rebase-landed-then-edited allow)
  • bash tests/fm-pr-merge.test.sh (10 ok) — covers the PR-head path now sharing the deduped subject_patches_are_in_reference()
  • bash tests/fm-gate-refuse.test.sh (7 ok)
  • bash tests/fm-teardown-endpoint-safety.test.sh (7 ok)
  • bash tests/fm-secondmate-safety.test.sh (77 ok)
  • Manual A/B CLI demo: built a local-only project where a second chain lands via real git rebase + git merge --ff-only (the fm-merge-local.sh flow), then ran bin/fm-teardown.sh task-x1 against the base-commit script and the HEAD script on the identical fixture
  • Manual safety demo on the fixed script: (1) one commit landed by rebase plus a later never-landed commit, (2) all patches landed but worktree dirty, (3) plain unmerged work — each run through bin/fm-teardown.sh task-x1
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 127)
✅ **Push** - passed

✅ No issues found.

fm-teardown.sh proved landed work by asking whether the branch's own
commits are reachable from the default branch. A rebase gives every
commit a new object id, so once landing rewrote them that question can
never be answered yes again, and teardown refused work that was fully in
main. Because bin/fm-merge-local.sh is fast-forward-only, rebasing onto a
moved default branch is the normal way a local-only chain lands, so every
multi-chain graph left earlier nodes permanently unreclaimable. The
documented remedies were all wrong for that state: merging again is
redundant, and --force would need captain authority to discard work that
was never at risk.

Add patch equivalence - the relationship `git cherry` reports - as an
additional landed-work proof, in the local-only merged check and in
work_is_landed alike. It is deliberately strict so it can only turn a
refusal into an allow on proof: every commit the ref cannot reach must
match a patch id the ref does contribute, an unreadable or empty patch
(an empty commit, or a merge commit whose conflict resolution `git show`
does not emit) never counts as landed, and a ref contributing no patches
at all stays inconclusive. The uncommitted-changes refusal and the
explicit-authority gate on --force are untouched.

Tests cover the three distinguishing cases: a branch whose patch landed
under a rewritten commit is reclaimable; a branch holding a commit whose
patch is absent from main still refuses; and a worktree with uncommitted
changes still refuses even when every commit's patch landed. Mutation
runs confirm each case fails on its own weakening - relaxing the match
from every commit to any commit fails the second, and dropping the
uncommitted check fails the third. A fourth case covers the same rebase
fix on the PR path, where main edited the file after the patch landed so
the whole-tree content check cannot conclude anything.
fm-teardown.sh proved landed work by asking whether the branch's own
commits are reachable from the default branch. A rebase gives every
commit a new object id, so once landing rewrote them that question can
never be answered yes again, and teardown refused work that was fully in
main. Because bin/fm-merge-local.sh is fast-forward-only, rebasing onto a
moved default branch is the normal way a local-only chain lands, so every
multi-chain graph left earlier nodes permanently unreclaimable. The
documented remedies were all wrong for that state: merging again is
redundant, and --force would need captain authority to discard work that
was never at risk.

Add patch equivalence - the relationship `git cherry` reports - as an
additional landed-work proof, in the local-only merged check and in
work_is_landed alike. It is deliberately strict so it can only turn a
refusal into an allow on proof: every commit the ref cannot reach must
match a patch id the ref does contribute, an unreadable or empty patch
(an empty commit, or a merge commit whose conflict resolution `git show`
does not emit) never counts as landed, and a ref contributing no patches
at all stays inconclusive. The uncommitted-changes refusal and the
explicit-authority gate on --force are untouched.

Tests cover the three distinguishing cases: a branch whose patch landed
under a rewritten commit is reclaimable; a branch holding a commit whose
patch is absent from main still refuses; and a worktree with uncommitted
changes still refuses even when every commit's patch landed. Mutation
runs confirm each case fails on its own weakening - relaxing the match
from every commit to any commit fails the second, and dropping the
uncommitted check fails the third. A fourth case covers the same rebase
fix on the PR path, where main edited the file after the patch landed so
the whole-tree content check cannot conclude anything.
…to fm/fm-teardown-false-refusal-after-rebase

# Conflicts:
#	bin/fm-teardown.sh
#	docs/gitlab-merge-watch.md
#	tests/fm-teardown.test.sh
bin/fm-lint.sh is the repo's canonical lint gate and CI invokes it directly,
so the branch has to leave it clean.

- Assert the numstat record carries its path field instead of parsing a
  partial line, which also uses the field the reader was discarding (SC2034).
- Give the subject-range locals explicit empty-string initialisers (SC1007).
- Check the reference-tree diff's exit status directly (SC2181).

All three are behavior-preserving or fail-closed: the new path assertion can
only turn a proof into a refusal, never a refusal into an allow.
@IanQiu979
IanQiu979 force-pushed the fm/fm-teardown-false-refusal-after-rebase branch from 2896c83 to 2c8dc6c Compare August 22, 2026 13:05
@greptile-apps

greptile-apps Bot commented Aug 22, 2026

Copy link
Copy Markdown

Confidence Score: 3/5

The PR should not merge until bundled repository overrides are rejected again; the unpaced signal test is an additional non-blocking reliability concern.

Extra merge arguments can bypass the canonical-repository guard through a short-option cluster and reach the merge CLI, while the modified signal test can flood Bash during trap execution and manufacture intermittent failures.

Files Needing Attention: bin/fm-pr-merge.sh, tests/fm-pr-merge.test.sh, tests/fm-remote-job.test.sh

Reviews (1): Last reviewed commit: "fix(teardown): clear shellcheck findings..." | Re-trigger Greptile

Comment thread bin/fm-pr-merge.sh
# A single-dash argument is a short-option cluster, which both CLIs expand
# one character at a time, so -yR carries --repo exactly as a bare -R does.
-*R*)
--repo|--repo=*|-R|-R?*)

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 security Bundled repository override bypass

When a caller supplies an extra argument such as -dR wrong/repo, the narrowed filter accepts it and forwards it after the URL-derived --repo, allowing the merge command to target a repository other than the canonical PR URL names.

How this was verified: The removed regression case identifies -dR as a repository override, while the new pattern no longer matches that cluster and the accepted arguments are forwarded verbatim.

Comment on lines 648 to 650
while kill -0 "$REPEAT_WORKER_PID" 2>/dev/null && [ "$SECONDS" -lt "$REPEAT_DEADLINE" ]; do
kill -TERM "$REPEAT_WORKER_PID" 2>/dev/null || true
sleep 0.05
done

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Unpaced SIGTERM flood

The tight loop now sends SIGTERM continuously throughout worker shutdown. This recreates the documented Bash pending-trap overload that can interrupt or corrupt shutdown, leave temporary ownership state behind, and produce flaky failures unrelated to the worker behavior under test.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

CRITICAL: gh pr diff vs current main REMOVES the GitLab merge path from bin/fm-pr-merge.sh / bin/fm-pr-lib.sh (reverts merged #2779) and drops the -yR short-option-cluster repo-override guard. The teardown rewrite (patches_are_in_ref / patch-id landing proof in bin/fm-teardown.sh) is the intended #2586 fix, but it is not landable on top of that revert. Ahead 23 / behind 1.

VISION (per rule):

  • One captain, one interface — teardown intent aligns (false refusal of rewritten-but-landed work is noise). The GitLab merge deletion is not that intent.
  • Authority is explicit — teardown --force gating is untouched, which aligns. Silently dropping GitLab merge capability does not.
  • Scripts own the mechanics — patch-equivalence as a scripted landed-work proof aligns. Mixing it with an unrelated merge-path revert does not.
  • A restart is a non-event — n/a.
  • Delegation with a spine — "unlanded work is never torn down" is the teardown goal. Reverting GitLab merge is out of scope and risks the fleet's merge surface.
  • The fleet outlives any vendor — regresses. GitLab merge watching/merging on arbitrary instances is deleted.
  • Scope — teardown rewrite is command-layer; deleting GitLab merge is not this task.

Class: corrective (teardown rewrite) but the GitLab merge revert is blocking and must not land.

Security: the -yR / bundled short-option cluster that used to reject --repo overrides is gone; extra merge args can bypass the canonical-repository guard. Combined with the GitLab merge deletion this is not a clean teardown-only diff.

Overlap / do not land together: bin/fm-teardown.sh and tests/fm-teardown.test.sh overlap teardown/spawn holds #2637 / #2692 / #2768 (and #2770 also edits teardown). Sequence separately even after the GitLab path is restored.

CI: no structured no-mistakes-pipeline-attestation:v1 matching THIS HEAD 2c8dc6cfddc3c298f345701b0e49d743730b551d. Require no-mistakes FAILED (run 32574776387). CI 32574776189 in progress after this-pass workflow approval; an Aug 18 CI success is a different HEAD and is not this gate. Greptile is not a merge gate.

Ahead 23, behind 1, mergeable, mergeStateStatus UNSTABLE.

Not merge-eligible. Waiting on the author to restore the GitLab merge path and keep the teardown rewrite on top of current main — not waiting on the captain.

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.

2 participants