Skip to content

fix(sync): bound the fetch without coreutils, and cover the hook - #67

Merged
LeTuR merged 1 commit into
Thurbeen:mainfrom
Pompedup:fix/sync-checkout-portable-timeout
Sep 11, 2026
Merged

fix(sync): bound the fetch without coreutils, and cover the hook#67
LeTuR merged 1 commit into
Thurbeen:mainfrom
Pompedup:fix/sync-checkout-portable-timeout

Conversation

@Pompedup

Copy link
Copy Markdown
Contributor

Intent

scripts/sync-checkout.sh bounded its fetch with timeout 15, which is GNU
coreutils and is not on a stock macOS. There it exits 127 — command not
found — which the guard cannot tell from a failed fetch. So every session on a
Mac reported could not reach origin (offline?) while the network was fine,
and then worked from a stale main.

That is the exact outcome the script exists to prevent, and it hid in the one
place nothing looks: the SessionStart hook runs before anyone is watching,
and an "offline" line reads as a blip, so nobody investigates.

Observed on a Mac where git ls-remote answered in 0.27s while the hook
reported the origin unreachable, and the checkout sat a commit behind for days.

What changed

fetch_bounded prefers timeout, falls back to Homebrew's gtimeout, and
otherwise runs a portable watchdog — polling rather than wait -n, since macOS
ships bash 3.2. The bound is kept deliberately: an unbounded fetch in a
session-start hook would hang the session open. The timeout is named once, as
FETCH_TIMEOUT_SECS.

scripts/sync-selftest.sh is new, and is why this could ship in the first
place — sync-checkout.sh was the only script in scripts/ with no coverage.
Each case runs against a throwaway origin on disk under a PATH holding only the
tools the script may use, so it reproduces the stock-macOS condition on any
platform and needs no network. That sandbox has a second effect worth keeping:
a tool the script starts reaching for without declaring shows up as a failure
rather than as a silent dependency on whatever the developer happened to have.

Beyond the regression it holds the contract around it:

  • a genuinely unreachable origin is still reported as unreachable;
  • each of the three refusals — dirty tree, feature branch, divergence — is
    reported and changes no tracked state;
  • a fast-forward that moves an INSTRUCTION_PATH raises restart-lead, and one
    that does not, does not;
  • the hook always gets one parseable JSON object and exit 0.

Wired into ./scripts/check.sh as sync, beside the other five selftests.

Verification

Written test-first. The selftest failed for the right reason before the fix —
the false "offline" swallows the script's entire behaviour, so all 11
assertions fell:

§1  FAIL  should NOT contain: could not reach origin
§4  FAIL  expected to find: the tree is dirty. Not fast-forwarding.
§5  FAIL  expected to find: restart-lead:

After the fix, all green, and check.sh shell is shellcheck-clean. The
coreutils case declares itself skipped rather than failed on a host with
neither timeout nor gtimeout — which is every stock macOS, and exactly the
host this bug came from.

Locally green: sync, shell, yaml, profiles, skills, voice. The full
check.sh was not completed in one piece on this machine — queue-selftest.sh
runs long and the session was restarted under it more than once — and pane
and markdown skip for want of lua and rumdl. CI runs the whole thing.

🤖 Generated with Claude Code

`scripts/sync-checkout.sh` bounded its fetch with `timeout 15`, which is GNU
coreutils and is not on a stock macOS. There it exited 127 — command not
found — which the guard could not tell from a failed fetch, so every session
on a Mac reported "could not reach origin (offline?)" while the network was
fine, and then worked from a stale `main`.

That is the exact outcome the script exists to prevent, and it hid in the one
place nothing looks: the SessionStart hook runs before anyone is watching, and
an "offline" line reads as a blip.

`fetch_bounded` now prefers `timeout`, falls back to Homebrew's `gtimeout`,
and otherwise runs a portable watchdog — polling rather than `wait -n`, since
macOS ships bash 3.2. The bound is kept: an unbounded fetch in a session-start
hook would hang the session open. The timeout is named once, as
FETCH_TIMEOUT_SECS.

`scripts/sync-selftest.sh` is new, and is why this could ship: sync-checkout
was the only script in scripts/ with no coverage. It runs every case against a
throwaway origin on disk under a PATH holding only the tools the script may
use, so it reproduces the stock-macOS condition on any platform and needs no
network. Beyond the regression it holds the contract around it — a real outage
is still reported, each of the three refusals changes no tracked state, a
fast-forward that brings instructions raises restart-lead, and the hook always
gets one parseable JSON object and exit 0.

Wired into `./scripts/check.sh` as `sync`, beside the other five selftests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@LeTuR
LeTuR enabled auto-merge (squash) September 11, 2026 11:55
@LeTuR
LeTuR merged commit 209a66c into Thurbeen:main Sep 11, 2026
11 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