harden: fail-closed PR gate, origin/HEAD refresh, worktree isolation - #10
Merged
Merged
Conversation
…D, assert worktree clone ownership Fix 1.2 (highest value): fm-pr-check.sh now fetches the PR before arming the merge poll and refuses (exit non-zero, naming the exact condition) when the PR body contains a skipped pipeline gate, an unresolved error, a high-risk marker, or when GitHub reports the merge state as DIRTY, or the PR targets a branch that differs from the project's true remote default. --force-ready bypasses the checks and records pr_check_override=1 in meta. Absence of no-mistakes markers (hand-written PRs, direct-PR mode) does not trip the check. Tests pin the false-positive/false-negative boundary across 13 cases. Fix 1.1: fm-fleet-sync.sh calls 'git remote set-head origin --auto' (best-effort) after every successful fetch, so origin/HEAD stays accurate even when the upstream repo changes its default branch. default_branch() now emits an actionable warning when it falls back to the hardcoded main/master scan. The STUCK report for a clone on a named non-default branch includes the one checkout command needed to restore it. Tests pin both behaviours. Fix 1.3: validate_spawn_worktree in fm-spawn.sh asserts that the acquired worktree's git-common-dir resolves inside the project directory firstmate spawned from. Treehouse keys its pool by basename+remote-url, so two local clones of the same remote share one pool; without this assertion a treehouse get can silently hand out a worktree backed by a different clone's .git.
…iable path Previously the gate failed open in three paths: gh absent from PATH skipped the check entirely, any gh call failure left REFUSE=0 via || VAR="", and a missing project= in meta silently skipped the base-branch comparison. A gate that passes when it cannot verify is the same defect that caused the nm-orchestration incident. Every unverifiable path now calls pr_check_refuse() with a distinct message: - gh not on PATH - gh pr view body/merge-state/base call fails - project= absent from task meta or directory not found - ls-remote fails or returns no symbolic ref --force-ready remains the only bypass and records pr_check_override=1. Six new tests cover each refused path (19 total, all passing, shellcheck clean).
…able paths
The cross-clone pool-leak check in validate_spawn_worktree previously failed
open in two paths: an empty result from git rev-parse --git-common-dir (rev-parse
fails) silently skipped the whole block, and an unresolvable path (cd fails)
skipped the comparison. Either miss could accept a worktree backed by a different
clone's .git objects.
Both paths now call explicit refuses:
- empty wt_common: "cannot verify worktree isolation: 'git rev-parse
--git-common-dir' failed or returned empty for '$WT'"
- unresolvable wt_common_real: "cannot verify worktree isolation:
git-common-dir path '$wt_common' cannot be resolved to an absolute path"
Two new tests in tests/fm-backend.test.sh pin each refuse path using a git stub
that intercepts --git-common-dir while delegating all other git calls to the real
binary. Both tests pass; shellcheck clean.
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.
Summary
Three hardening fixes derived from the nm-orchestration incident (2026-07-14, PR #64), where a PR whose own body admitted "Test skipped / 1 error still open / 🚨 High risk" was relayed to the captain as ready because
fm-pr-check.shnever read the PR body before arming the merge poll.Fix 1.1 —
bin/fm-fleet-sync.sh: callsgit remote set-head origin --auto(best-effort) after every successful fetch soorigin/HEADstays accurate when the upstream repo renames its default branch.default_branch()emits an actionable warning with the repair command when it falls back to the hardcoded main/master scan.The STUCK report for a clone on a non-default branch includes the one
git checkoutcommand needed to restore it.Fix 1.2 —
bin/fm-pr-check.sh(highest value): the gate is fully fail-closed.Seven distinct refuse paths cover every unverifiable condition:
ghnot on PATH, anygh pr viewcall fails (auth error, network, rate limit), PR body contains a no-mistakes skip marker / unresolved error / high-risk label, GitHub reports merge state DIRTY,project=absent from task meta or directory not found,ls-remotefails,ls-remotereturns no symbolic ref.--force-readyis the only bypass and recordspr_check_override=1in meta.19 tests cover all refuse paths plus false-positive boundaries and bookkeeping; shellcheck clean.
Fix 1.3 —
bin/fm-spawn.sh:validate_spawn_worktreeasserts the acquired worktree'sgit-common-dirresolves inside the project directory.Both unverifiable paths are now fail-closed: an empty result from
git rev-parse --git-common-dirand an unresolvable path fromcdeach refuse with a distinct message naming the exact failure.Two new tests in
tests/fm-backend.test.shpin each refuse path; shellcheck clean.Test plan
bash tests/fm-pr-check.test.sh— 19/19 passbash tests/fm-fleet-sync.test.sh— all pass (including 2 new tests for Fix 1.1)bash tests/fm-backend.test.sh— all pass (including 2 new tests for Fix 1.3)shellcheck -x bin/fm-pr-check.sh bin/fm-fleet-sync.sh bin/fm-spawn.sh tests/fm-pr-check.test.sh tests/fm-fleet-sync.test.sh tests/fm-backend.test.sh— clean