Skip to content

fix(bin): keep teardown's landed-work check working on git older than 2.38 - #9

Merged
doitdigital0495 merged 2 commits into
mainfrom
fm/fm-teardown-test-local-fail
Aug 23, 2026
Merged

fix(bin): keep teardown's landed-work check working on git older than 2.38#9
doitdigital0495 merged 2 commits into
mainfrom
fm/fm-teardown-test-local-fail

Conversation

@doitdigital0495

Copy link
Copy Markdown
Owner

Intent

Fix the environment-dependent fm-teardown.test.sh failure that fails locally on this WSL machine while passing in GitHub CI, so it cannot abort the suite and hide later regressions.

Required approach, all of which was done before this run: reproduce the failure locally through the real test entry point (bin/fm-test-run.sh) before changing code; capture the exact expected behavior, observed failure, inputs, and repeatability; separate the initiating test operation, the local environment condition exposing it, and the visible early suite abort; compare the failing WSL path against the proven GitHub CI path; test the smallest counterfactual for each likely environment dependency; seek disconfirming evidence rather than assuming filesystem, git configuration, shell version, locale, or worktree layout is responsible; and confirm current behavior after the recent upstream merge rather than relying on an old observation from base 1cb900c.

Two distinct environment-dependent root causes were found and fixed.

  1. Product bug behind the brief's 'content-landed' case. bin/fm-teardown.sh's content_in_default used 'git merge-tree --write-tree', which exists only from git 2.38. This host runs git 2.34 (Ubuntu 22.04 and its WSL image), where the call exits 128 and the whole landed-work content fallback silently answered 'not landed'. That is a real product bug, not just a test bug: on any such host teardown refuses genuinely landed work. The fix keeps the full merge where git provides it and falls back to a 3-way 'read-tree -i -m' into a throwaway index otherwise. The fallback touches neither the worktree nor the real index, and resolves only trivial merges: a merge it cannot resolve leaves unmerged stages, write-tree fails, and the caller refuses, the same direction a merge conflict already takes. The temp index is a mktemp -d directory because git rejects the zero-length file mktemp would otherwise create. The hybrid shape is deliberate: dropping every host to the conservative trivial merge would make teardown newly refuse a squash-merged branch whose default branch later gained overlapping edits in the same file, and that behavior could not be re-verified locally on git 2.34.

  2. Test-host leak that, after the upstream merge, actually aborted the suite first at case 14 of 65, before the content-landed case ever ran. tests/fm-teardown.test.sh's run_teardown overrode state and config but not FM_HOME, and teardown resolves its data dir, secondmate registry and Relay .env from FM_HOME. Every session firstmate itself launches exports one, so the run answered from the operator's real ~/.firstmate registry and refused with 'no registry binding for secondmate task-x1'. FM_HOME is now pinned to a per-case sandbox directory. It is deliberately NOT the case dir itself, because teardown refuses removal targets inside the active firstmate home and the case worktrees live there.

Constraints honored: do not weaken teardown's content-landed, uncommitted-work, or unlanded-work protections; prefer making the test deterministic across supported environments over a skip; keep the change minimal and portable; leave executable regression coverage that fails under the identified local condition and still exercises all later cases.

Regression coverage added: two new cases (content-landed-old-git, content-unlanded-old-git) shim 'merge-tree' away from git entirely so a modern git also proves the fallback path and proves it still refuses genuinely unlanded work; and the existing forced-secondmate herdr child preflight case now exports a decoy ambient FM_HOME carrying a foreign secondmate registry, so the FM_HOME pin is asserted rather than assumed. Counterfactuals were run: removing the pin makes that case fail both with and without an ambient FM_HOME, and the merge-tree shim was verified to intercept every global-option form while passing all other git commands through.

One line of durable knowledge was recorded in .agents/skills/firstmate-coding-guidelines/SKILL.md under 'Compatibility and enforcement': external tool version is a compatibility axis, scripts must run on the oldest release a supported host ships, and a newer subcommand kept for its better behavior needs an older fallback covered by a test that shims the newer one away.

Validation already performed: the fm-teardown suite is green three times through bin/fm-test-run.sh, both with the ambient firstmate home present and with it unset (the CI-like condition); the pr-forge family is 5/5; bin/fm-lint.sh and bin/fm-doc-audience-check.sh are clean.

Evidence to classify, not hide: the portable lanes on this machine are broadly red independently of this change. 21 scripts fail (portable-serial 19/120, plus fm-test-run.test.sh and fm-arm-pretool-check.test.sh). None of them touch the three files this change edits. Spot-checking fm-update, fm-secondmate-sync, fm-session-start and fm-fleet-sync shows they fail with FM_HOME unset too, so they are a different cause: fm-update.test.sh specifically fails because git init defaults to master on this host and because a global check-agents-e2e pre-commit hook fires inside the test's own fixture repos. Those are separate pre-existing host-config leaks, out of scope for this task, and are reported rather than fixed here.

Delivery: open the PR against doitdigital0495/firstmate, never upstream. Drive every gate through the CI-ready return point. Escalate any ask-user finding instead of deciding it, and never use --yes.

What Changed

  • bin/fm-teardown.sh's content_in_default no longer depends on git merge-tree --write-tree (git 2.38+). It still uses that path where git provides it, and otherwise falls back to a new trivially_merged_tree helper that 3-way merges via read-tree -i -m into a throwaway index directory. On hosts shipping git 2.34 (Ubuntu 22.04 / WSL) the old call exited 128 and the check silently answered "not landed", so teardown refused genuinely landed work. Unresolvable merges leave unmerged stages, write-tree fails, and the caller still refuses.
  • tests/fm-teardown.test.sh now pins FM_HOME to a per-case sandbox (not the case dir, which teardown refuses as inside the active firstmate home), so runs no longer resolve the data dir, secondmate registry and Relay .env from the operator's real ~/.firstmate and abort the suite early. The forced-secondmate herdr child preflight case exports a decoy ambient FM_HOME with a foreign registry to assert the pin.
  • Added content-landed-old-git and content-unlanded-old-git cases that shim merge-tree away from git so a modern git also exercises the fallback and proves it still refuses unlanded work; recorded the external-tool-version compatibility rule in .agents/skills/firstmate-coding-guidelines/SKILL.md.

Risk Assessment

✅ Low: Bounded three-file change: the new merge fallback is strictly more conservative than the path it backstops (an unresolvable merge leaves unmerged stages, write-tree fails, and teardown still refuses), it has the only merge-tree call site in the codebase so no sibling path stays broken, and both root causes carry executable regression cases that exercise real teardown behavior rather than source text.

Testing

Ran the fm-teardown suite through its real entry point in four configurations to separate the two root causes. At base the suite aborted after 13 cases at herdr-child-preflight with the operator's ambient FM_HOME present, and base teardown paired with the new tests failed the content-landed case with exit 1 instead of 0, reproducing the git-2.34 product bug where teardown refuses genuinely landed work; the host was confirmed to reject merge-tree --write-tree with exit 128. At the target commit the full script is green (64 cases, exit 0) both with the ambient firstmate home present and with FM_HOME unset, including the two new cases that shim merge-tree away and prove landed work is recognized while unlanded work is still refused. Evidence is CLI transcripts rather than visual artifacts because the change is a shell CLI teardown path with no rendered surface. The working tree was restored to HEAD and left clean.

Evidence: Before/after summary across the four runs

Source: Before/after summary across the four runs

host: git version 2.34.1 (Ubuntu 22.04 / WSL) ambient FM_HOME at test time: /home/daan/.firstmate BASE ba5f4c4 (script+tests), ambient FM_HOME: not ok - herdr-child-preflight: refusal did not explain its non-mutating boundary FM_TEST_SUMMARY total=1 failed=1 skipped_gate=0 duration_ms=28085 cases passed before abort: 13 BASE bin/fm-teardown.sh + NEW tests (isolates the git-2.34 product bug): not ok - content-landed: teardown should succeed when content is already in the default branch: expected exit 0, got 1 FM_TEST_SUMMARY total=1 failed=1 skipped_gate=0 duration_ms=56550 TARGET 59b863c, ambient FM_HOME present: FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=122158 ok - content fallback recognizes landed work on a git without merge-tree --write-tree ok - the merge-tree-free content check still refuses genuinely unlanded work cases passed: 64 TARGET 59b863c, FM_HOME unset (CI-like): FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=123163 cases passed: 64

host: git version 2.34.1 (Ubuntu 22.04 / WSL)
ambient FM_HOME at test time: /home/daan/.firstmate

BASE ba5f4c4 (script+tests), ambient FM_HOME:
not ok - herdr-child-preflight: refusal did not explain its non-mutating boundary
FM_TEST_SUMMARY total=1 failed=1 skipped_gate=0 duration_ms=28085
  cases passed before abort: 13

BASE bin/fm-teardown.sh + NEW tests (isolates the git-2.34 product bug):
not ok - content-landed: teardown should succeed when content is already in the default branch: expected exit 0, got 1
FM_TEST_SUMMARY total=1 failed=1 skipped_gate=0 duration_ms=56550

TARGET 59b863c, ambient FM_HOME present:
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=122158
ok - content fallback recognizes landed work on a git without merge-tree --write-tree
ok - the merge-tree-free content check still refuses genuinely unlanded work
  cases passed: 64

TARGET 59b863c, FM_HOME unset (CI-like):
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=123163
  cases passed: 64
Evidence: Host git rejects merge-tree --write-tree

Source: Host git rejects merge-tree --write-tree

# host git: git version 2.34.1 $ git merge-tree --write-tree master HEAD fatal: unknown rev --write-tree exit=128

# host git: git version 2.34.1
$ git merge-tree --write-tree master HEAD
fatal: unknown rev --write-tree
exit=128
Evidence: Base run: suite aborts at case 14 (FM_HOME leak)

Source: Base run: suite aborts at case 14 (FM_HOME leak)

FM_TEST_BEGIN 2026-08-23T22:07:59Z tests/fm-teardown.test.sh family=pr-forge expected_gate_skip=none
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 121ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 114ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - local-only worktree with HEAD on a fork remote is torn down (fix holds)
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 123ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - teardown prompts tasks-axi backlog refresh when compatible
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 117ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - teardown honors config/backlog-backend=manual even when tasks-axi is compatible
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 119ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 115ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - local-only worktree with truly unpushed work is refused (safety preserved)
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 119ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 118ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - local-only worktree with work merged into local main is torn down (no regression)
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 117ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 115ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - no-mistakes worktree with HEAD on origin is torn down (no regression)
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 119ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~6 bytes (6 bytes) in 113ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - no-mistakes worktree with genuinely unlanded work is refused (safety preserved)
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 117ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 116ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - local-only worktree with unpushed work is torn down under --force (escape hatch)
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 118ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - teardown completes when an exact busy-state sidecar is already absent
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 112ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - herdr teardown removes pane-owned escalation dedupe state
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 116ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - herdr flat teardown refuses before returning the isolated copy under lock contention and the retry completes cleanly
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 116ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - herdr flat teardown never erases records when pane presence is unparseable
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 116ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 113ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 118ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 105ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - herdr flat teardown preflight refuses before every destructive change
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 117ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
not ok - herdr-child-preflight: refusal did not explain its non-mutating boundary
FM_TEST_END 2026-08-23T22:08:27Z tests/fm-teardown.test.sh exit=1 duration_ms=28058 gate_skip=false
FM_TEST_SUMMARY total=1 failed=1 skipped_gate=0 duration_ms=28085
FM_TEST_SUMMARY_FAMILY family=pr-forge count=1 duration_ms=28058 failed=1
FM_TEST_SLOWEST rank=1 script=tests/fm-teardown.test.sh duration_ms=28058
Evidence: Base teardown script + new tests: landed work wrongly refused on git 2.34

Source: Base teardown script + new tests: landed work wrongly refused on git 2.34

FM_TEST_BEGIN 2026-08-23T22:08:54Z tests/fm-teardown.test.sh family=pr-forge expected_gate_skip=none
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 117ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 121ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - local-only worktree with HEAD on a fork remote is torn down (fix holds)
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 125ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - teardown prompts tasks-axi backlog refresh when compatible
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 121ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - teardown honors config/backlog-backend=manual even when tasks-axi is compatible
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 119ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 115ms�[0m
�[90m12:08AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - local-only worktree with truly unpushed work is refused (safety preserved)
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 234ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 198ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - local-only worktree with work merged into local main is torn down (no regression)
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 241ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 205ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - no-mistakes worktree with HEAD on origin is torn down (no regression)
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 148ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~6 bytes (6 bytes) in 487ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - no-mistakes worktree with genuinely unlanded work is refused (safety preserved)
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 337ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 234ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - local-only worktree with unpushed work is torn down under --force (escape hatch)
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 121ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - teardown completes when an exact busy-state sidecar is already absent
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 348ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - herdr teardown removes pane-owned escalation dedupe state
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 115ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - herdr flat teardown refuses before returning the isolated copy under lock contention and the retry completes cleanly
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 121ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - herdr flat teardown never erases records when pane presence is unparseable
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 117ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 119ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 115ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 118ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - herdr flat teardown preflight refuses before every destructive change
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 119ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - forced secondmate teardown preflights every Herdr child before cleanup mutation
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 117ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - forced secondmate teardown holds every descendant lifecycle and metadata lock
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 120ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - forced secondmate teardown retains Herdr child identity until exact pane disappearance
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 120ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - forced teardown retains a nested secondmate home and its grandchild's Herdr identity when the grandchild close is unconfirmed
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 118ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - herdr projection teardown retires its journal only after confirming the exact recorded pane is gone
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 129ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - herdr projection teardown retains every record when post-close presence is unknown
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 118ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - herdr projection teardown surfaces failed focus restoration without turning confirmed cleanup into a hard failure
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 119ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~6 bytes (6 bytes) in 115ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - squash-merged + deleted-branch worktree (PR merged) is torn down (the fix)
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 119ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~6 bytes (6 bytes) in 116ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - squash-merged PR accepts a local HEAD that is an ancestor of the final PR head
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 112ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~6 bytes (6 bytes) in 121ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~6 bytes (6 bytes) in 119ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - teardown discovers a merged PR by branch name and tears down when no pr= was ever recorded
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 112ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~7 bytes (7 bytes) in 120ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~6 bytes (6 bytes) in 118ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~6 bytes (6 bytes) in 110ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - squash-merged PR accepts replayed unpushed local patches contained in the PR head
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 136ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~6 bytes (6 bytes) in 129ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~11 bytes (11 bytes) in 122ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - merged PR does not allow teardown after a later local commit
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 193ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~6 bytes (6 bytes) in 124ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~11 bytes (11 bytes) in 112ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - fm-pr-check does not refresh PR head after HEAD moves
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 135ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~6 bytes (6 bytes) in 117ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
ok - fm-pr-check records the remote PR head when the local worktree lags
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~0 bytes (0) in 110ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~6 bytes (6 bytes) in 111ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1m0 commits scanned.�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mscanned ~6 bytes (6 bytes) in 121ms�[0m
�[90m12:09AM�[0m �[32mINF�[0m �[1mno leaks found�[0m
not ok - content-landed: teardown should succeed when content is already in the default branch: expected exit 0, got 1
FM_TEST_END 2026-08-23T22:09:50Z tests/fm-teardown.test.sh exit=1 duration_ms=56523 gate_skip=false
FM_TEST_SUMMARY total=1 failed=1 skipped_gate=0 duration_ms=56550
FM_TEST_SUMMARY_FAMILY family=pr-forge count=1 duration_ms=56523 failed=1
FM_TEST_SLOWEST rank=1 script=tests/fm-teardown.test.sh duration_ms=56523
- Evidence: [Target run with ambient FM_HOME: 64 cases green](https://github.com/doitdigital0495/firstmate/blob/19104bef405e3773031203eff6cde4b9fb9bb927/.no-mistakes/evidence/fm/fm-teardown-test-local-fail/fixed-fm-teardown-ambient-fmhome.log) - Evidence: [Target run with FM_HOME unset (CI-like): 64 cases green](https://github.com/doitdigital0495/firstmate/blob/19104bef405e3773031203eff6cde4b9fb9bb927/.no-mistakes/evidence/fm/fm-teardown-test-local-fail/fixed-fm-teardown-no-fmhome.log)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 3 infos
  • ℹ️ bin/fm-teardown.sh:927 - Any non-zero merge-tree --write-tree exit now routes into trivially_merged_tree, conflating "subcommand missing" (exit 128/129) with "real merge conflict" (exit 1). The outcome is unchanged - a path merge-tree conflicts on is by definition not trivially resolvable, so read-tree -i -m leaves unmerged stages, write-tree fails and the caller still refuses - but on a modern git a genuine conflict now pays an extra merge-base + read-tree + write-tree round trip before reaching the same refusal, and the code no longer says which failure it is handling. Branching on the exit status (fall back only when merge-tree is unsupported) would keep intent visible.
  • ℹ️ bin/fm-teardown.sh:894 - The throwaway index directory is removed inline but is not registered with the existing EXIT trap (trap teardown_release_locks EXIT, line 209). If teardown is interrupted between mktemp -d and rm -rf, a /tmp/fm-teardown-merge-index.* directory leaks. The normal path is clean; this only bites on signal/abort during the content check.
  • ℹ️ tests/fm-teardown.test.sh:1872 - Pinning FM_HOME to an always-empty $case_dir/home means $SECONDMATE_REG never exists in any case, so the registry-binding refusal in validate_firstmate_home_for_removal (bin/fm-teardown.sh:1787) is now skipped by the [ -e "$SECONDMATE_REG" ] guard in every test. The decoy home only proves the pin overrides an ambient FM_HOME; nothing asserts teardown still refuses when its own pinned registry carries a mismatched binding. This matches CI's prior behavior (FM_HOME unset resolved to a repo root with no data/secondmates.md), so it is not a regression against the green baseline - it does make the loss of that coverage permanent rather than host-dependent.
✅ **Test** - passed

✅ No issues found.

  • bash bin/fm-test-run.sh tests/fm-teardown.test.sh at target 59b863c with ambient FM_HOME=/home/daan/.firstmate -> exit 0, 64 cases ok
  • env -u FM_HOME bash bin/fm-test-run.sh tests/fm-teardown.test.sh at target (CI-like, no ambient home) -> exit 0, 64 cases ok
  • Counterfactual: git checkout ba5f4c4 -- bin/fm-teardown.sh tests/fm-teardown.test.sh then bash bin/fm-test-run.sh tests/fm-teardown.test.sh -> exit 1, aborts at case herdr-child-preflight after 13 passing cases (FM_HOME leak reproduced)
  • Isolation of the product bug: base bin/fm-teardown.sh + target tests/fm-teardown.test.sh -> not ok - content-landed: teardown should succeed when content is already in the default branch: expected exit 0, got 1
  • Target-only regression cases observed passing: content fallback recognizes landed work on a git without merge-tree --write-tree and the merge-tree-free content check still refuses genuinely unlanded work
  • Host environment check: git --version (2.34.1) and git merge-tree --write-tree master HEAD in a throwaway repo -> fatal: unknown rev --write-tree, exit 128
  • git status --short after restoring both files to HEAD -> clean
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…rite-tree

The landed-work content fallback used `git merge-tree --write-tree`, which
exists only from git 2.38. Ubuntu 22.04 and its WSL image still ship git 2.34,
where that call fails with a usage error and the whole check silently answered
"not landed": teardown then refused genuinely landed work on every such host.

Keep the full merge where git has it and fall back to a 3-way `read-tree -i -m`
into a throwaway index otherwise. The fallback touches neither the worktree nor
the real index, and resolves only trivial merges - a merge it cannot resolve
leaves unmerged stages, `write-tree` fails, and the caller refuses, the same
direction a merge conflict already takes.

Two test fixes go with it, both for verdicts that depended on the test host:

- Pin FM_HOME to a per-case sandbox. Teardown resolves its data dir, secondmate
  registry and Relay .env from FM_HOME, so an ambient one - every session
  firstmate itself launches exports one - made the run answer from the
  operator's real home. That refused at the secondmate registry binding and
  aborted the suite before the later cases ran at all.
- Cover the content check with the newer subcommand shimmed away, so a modern
  git also proves the fallback, plus its refusal of genuinely unlanded work.

Record the external-tool version axis in firstmate-coding-guidelines so the next
change checks the oldest supported git before depending on a subcommand.
@doitdigital0495
doitdigital0495 merged commit 69e445c into main Aug 23, 2026
13 checks passed
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