fix(bin): resolve BSD stat by path on Darwin (Homebrew coreutils silently kills the watcher) - #2906
Open
ashield wants to merge 1 commit into
Open
fix(bin): resolve BSD stat by path on Darwin (Homebrew coreutils silently kills the watcher)#2906ashield wants to merge 1 commit into
ashield wants to merge 1 commit into
Conversation
`uname` tells you the OS, not which `stat` wins on PATH. On a Mac with
Homebrew coreutils, /opt/homebrew/opt/coreutils/libexec/gnubin precedes
/usr/bin, so `uname` reports Darwin while `stat` is GNU coreutils 9.x.
Every `[ "$(uname)" = Darwin ] && stat -f ...` branch then hits exactly the
failure bin/fm-watch.sh already documents for the `stat -f || stat -c`
fallback form: GNU `stat -f` is *filesystem* stat, so it writes a partial
filesystem dump to stdout before failing, and arithmetic under `set -u`
aborts on the stray token.
Reproduced against origin/main with gnubin ahead of /usr/bin:
mtime returned: [ File: "bin/fm-watch.sh"
ID: 100000d0000001a Namelen: ? Type: apfs
Block size: 4096 Fundamental block size: 4096
...]
arithmetic under set -u: bash: File: unbound variable
With the fix, same PATH:
mtime returned: [1787532376]
arithmetic under set -u: 0
Observed impact: on such a host every `fm-watch.sh` cycle exits 1 within a
second, `beacon_age` stays 999999, and autoarm eventually records
`outcome=failed-suppressed`. Event-driven supervision is silently dead
while the agent still answers direct messages, so a fleet only discovers it
when routed work stops being noticed.
Resolves BSD stat by path at all 39 call sites across 23 files in bin/,
overridable via FM_BSD_STAT for hosts that keep BSD stat elsewhere. The
`stat -c` (GNU) branches are untouched, and the two probe-style sites
behave identically on Linux, where /usr/bin/stat is the same binary bare
`stat` already resolved to.
Confidence Score: 5/5The PR appears safe to merge; the changed calls consistently select the intended BSD stat implementation on Darwin while preserving existing Linux behavior and explicit override support. No concrete changed-code failure remains after checking the child-shell boundary, override propagation, platform branches, and repository rules. Reviews (1): Last reviewed commit: "fix(bin): resolve BSD stat by path on Da..." | Re-trigger Greptile |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On a Mac with Homebrew coreutils installed,
/opt/homebrew/opt/coreutils/libexec/gnubinprecedes/usr/binon PATH.unamestill reportsDarwin, butstatis GNU coreutils 9.x.Every
[ "$(uname)" = Darwin ] && stat -f ...branch then hits precisely the failurebin/fm-watch.shalready documents for thestat -f || stat -cfallback form — GNUstat -fis filesystem stat, so it writes a partial filesystem dump to stdout before failing, and arithmetic underset -uaborts on the stray token.The existing comment diagnoses the hazard correctly but keys the decision on the OS rather than on which
statbinary wins.unamecannot answer that question.Reproduction
Against
main, with gnubin ahead of/usr/bin:With this change, same PATH:
Observed impact
Found on a real remote second-mate host, not synthetically. Every
fm-watch.shcycle exited 1 within a second,beacon_agestayed pinned at999999, and.claude-autoarm-epocheventually recordedoutcome=failed-suppressed:The failure mode is quiet, which is what makes it worth fixing: the agent still answers direct messages, so the home looks healthy. Only event-driven supervision is dead — the fleet discovers it when routed work stops being noticed.
It is also easy to miss when debugging over SSH, because a non-interactive SSH PATH usually has no gnubin. Arming by hand returns
exit_code=0while the GUI-session agent keeps failing.The change
Resolves BSD stat by path at all 39 call sites across 23 files in
bin/, overridable viaFM_BSD_STATfor hosts that keep BSD stat elsewhere:stat -c(GNU) branches are untouched.fm-test-isolation-proof.sh,fm-x-lib.sh) behave identically on Linux, where/usr/bin/statis the same binary barestatalready resolved to.fm-fleet-snapshot.shis alreadystat_style-guarded.fm-watch.shcomment to record why OS detection is necessary but not sufficient.Verification
bin/fm-lint.shclean (ShellCheck 0.11.0 pinned, actionlint 1.7.12, 3 workflows valid)fm-watch-arm.test.sh,fm-watcher-lock.test.sh,fm-watch-checkpoint.test.shall pass