Skip to content

feat(bootstrap): detect primary checkout drift from its own origin#5

Merged
Freudator86 merged 3 commits into
mainfrom
fm/fm-self-drift-check
Jul 20, 2026
Merged

feat(bootstrap): detect primary checkout drift from its own origin#5
Freudator86 merged 3 commits into
mainfrom
fm/fm-self-drift-check

Conversation

@Freudator86

Copy link
Copy Markdown
Owner

Intent

Add a session-start bootstrap diagnostic for drift between the primary Firstmate checkout's checked-out default branch at FM_ROOT and its own origin/default branch, which existing project fleet sync and upstream-template/fork checks do not cover. The check must be detect-only: never fast-forward, checkout, merge, stash, or discard primary work; it should fetch origin under a bounded timeout, silently skip no-origin, fetch-failure, timeout, detached, and off-default states, and avoid duplicating TANGLE. On a matching branch it stays silent; on ahead-only, behind-only, or diverged history it emits a distinct SELF_DRIFT line with both commit counts, using rev-list and merge-base logic. Keep it stateless, run it in both locked and lock-refused detect-only bootstrap paths, add focused tests for every requested state including slow origin, and document proportional remediation through bootstrap-diagnostics and AGENTS.md: /updatefirstmate only for a clean fast-forward, otherwise a manual preserve-and-merge crewmate task.

What Changed

  • Add self_drift_check to bin/fm-bootstrap.sh: a detect-only diagnostic that fetches origin under a bounded, configurable timeout (FM_SELF_DRIFT_BOOTSTRAP_TIMEOUT, default 10s) and compares the primary checkout's default branch against origin/<default> using rev-list/merge-base, emitting a SELF_DRIFT: ... ahead, ... behind ... (ahead|behind|diverged) line when they differ. It never fetches beyond the bound, never checks out/merges/fast-forwards/stashes, and silently no-ops on no-origin, non-default/detached HEAD, fetch failure, and timeout — running only in the non-TANGLE branch of the bootstrap flow so it doesn't duplicate TANGLE's off-default detection.
  • Add tests/fm-self-drift.test.sh covering matching, ahead-only, behind-only, diverged, no-origin, off-default, detached-HEAD, timeout (slow origin), and fetch-failure states.
  • Document the new SELF_DRIFT diagnostic and its proportional remediation (/updatefirstmate for a clean fast-forward, otherwise a manual preserve-and-merge crewmate task) in .agents/skills/bootstrap-diagnostics/SKILL.md, AGENTS.md, docs/architecture.md, and docs/configuration.md.

Risk Assessment

✅ Low: This round only adds two well-targeted tests (detached HEAD, plain fetch failure) that correctly exercise the code paths they claim to cover, fully resolving the prior round's test-coverage gap with no source changes and no new issues.

Testing

Baseline full e2e suite (tests/*.test.sh under tmux) already passed. I additionally ran the new tests/fm-self-drift.test.sh directly (9/9 pass, covering matching/ahead/behind/diverged/no-origin/off-default/detached/fetch-failure/slow-origin), re-ran tests/fm-bootstrap.test.sh and tests/fm-captain-translation-contract.test.sh to confirm no regression (21/21 and 7/7 checks pass, identical to base-commit behavior), and captured a manual CLI transcript exercising the real bin/fm-bootstrap.sh against live git fixtures to visibly demonstrate the exact SELF_DRIFT/TANGLE output an end user sees for each drift state. All checks passed; no findings.

Evidence: CLI transcript of bin/fm-bootstrap.sh SELF_DRIFT diagnostic across matching, ahead-only, behind-only, diverged, and off-default(TANGLE) states

=== Case 1: primary matches origin -> bootstrap stays silent === (silent, as expected) === Case 2: primary has one unpushed local commit -> ahead-only, quantified === SELF_DRIFT: primary checkout default branch 'main' is 1 ahead, 0 behind origin/main (ahead) - needs attention === Case 3: origin gains a commit via writer push -> behind-only, quantified === SELF_DRIFT: primary checkout default branch 'main' is 0 ahead, 1 behind origin/main (behind) - needs attention === Case 4: primary also behind after writer push -> diverged === SELF_DRIFT: primary checkout default branch 'main' is 1 ahead, 1 behind origin/main (diverged) - needs attention === Case 5: primary stranded on a feature branch -> TANGLE only, no duplicate SELF_DRIFT === TANGLE: primary checkout on feature branch 'fm/feature' (expected 'main'); the work is safe on that ref - read-only session must leave restore work to the session holding the fleet lock

=== Setup: seed repo, bare origin, primary + writer clones ===

=== Case 1: primary matches origin -> bootstrap stays silent (no SELF_DRIFT line) ===
(silent, as expected)

=== Case 2: primary has one unpushed local commit -> ahead-only, quantified ===
SELF_DRIFT: primary checkout default branch 'main' is 1 ahead, 0 behind origin/main (ahead) - needs attention

=== Case 3: fresh primary, origin gains a commit via writer push -> behind-only, quantified ===
SELF_DRIFT: primary checkout default branch 'main' is 0 ahead, 1 behind origin/main (behind) - needs attention

=== Case 4: primary (already ahead from case 2) also behind after writer push -> diverged ===
SELF_DRIFT: primary checkout default branch 'main' is 1 ahead, 1 behind origin/main (diverged) - needs attention

=== Case 5: primary stranded on a feature branch -> TANGLE only, no duplicate SELF_DRIFT ===
TANGLE: primary checkout on feature branch 'fm/feature' (expected 'main'); the work is safe on that ref - read-only session must leave restore work to the session holding the fleet lock

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • ⚠️ tests/fm-self-drift.test.sh:1 - The user intent requires: "add focused tests for every requested state including slow origin," and the requested silent-skip states are explicitly enumerated as "no-origin, fetch-failure, timeout, detached, and off-default states." tests/fm-self-drift.test.sh covers matching/ahead/behind/diverged, no-origin, off-default, and timeout (slow origin), but has no test for a detached-HEAD primary (self_drift_check's [ &#34;$current&#34; = &#34;$default&#34; ] skip at bin/fm-bootstrap.sh:212, distinct from TANGLE's own detached-HEAD handling) nor for a plain non-timeout fetch failure (the wait &#34;$pid&#34; nonzero-exit path at bin/fm-bootstrap.sh:232-234, as opposed to the timeout/kill path already tested by test_slow_origin_is_bounded_and_silent). Two of the five explicitly-named required states are untested.
  • ℹ️ bin/fm-bootstrap.sh:214 - self_drift_check's bounded-background-job timeout logic (bin/fm-bootstrap.sh:214-236: set -m save/restore, background job, jobs -r -p poll loop, kill -TERM on the process group, wait/reap) is a near-verbatim duplicate of the existing pattern in fleet_sync (bin/fm-bootstrap.sh:166-189). Could be factored into a shared helper (e.g. run_bounded(cmd, timeout) -&gt; exit status), but the two call sites differ slightly in how they consume output/exit status, so this is a nice-to-have rather than a required cleanup.

🔧 Fix: Add self-drift tests for detached HEAD and fetch failure
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"
  • bash tests/fm-self-drift.test.sh
  • bash tests/fm-bootstrap.test.sh
  • bash tests/fm-captain-translation-contract.test.sh
  • Manual CLI transcript: ran bin/fm-bootstrap.sh with FM_BOOTSTRAP_DETECT_ONLY=1 against real git fixtures for matching, ahead-only, behind-only, diverged, and off-default(TANGLE) primary states
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@Freudator86
Freudator86 merged commit b0b04d0 into main Jul 20, 2026
5 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