Skip to content

fix(bin): require project clone roots during fleet sync - #2849

Merged
kunchenguid merged 2 commits into
kunchenguid:mainfrom
karotkriss:fm/fm-2691-sync-guard
Aug 23, 2026
Merged

fix(bin): require project clone roots during fleet sync#2849
kunchenguid merged 2 commits into
kunchenguid:mainfrom
karotkriss:fm/fm-2691-sync-guard

Conversation

@karotkriss

@karotkriss karotkriss commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #2691

Intent

Fix a defect in bin/fm-fleet-sync.sh, reported as issue #2691: the project-clone refresh treats every directory under projects/ as a clone. Git repository discovery walks upward, so for a directory that is not itself a repository (for example a worktree container left under projects/, where the actual repository is one level below), every 'git -C

' resolves to the ENCLOSING repository. In a firstmate home that enclosing repository is the firstmate checkout itself, so a routine fleet refresh silently fetched, pruned and fast-forwarded firstmate's own default branch and reported the result under the unrelated project directory's label - an unrequested self-update outside the guarded update path, with a report that named the wrong repository.

The goal is the minimal, targeted guard the issue asks for: before any other git command runs against a candidate under projects/, require that the candidate is the root of its OWN work tree, comparing 'git rev-parse --show-toplevel' against the directory's own physical path. Anything else is skipped by name, loudly enough to be noticed, and the skip message names the repository that would otherwise have been touched so an operator can see what discovery resolved to. The existing 'skipped: not a git repo' wording and behavior for a candidate that resolves to no repository at all is deliberately left unchanged, so only the new enclosing-repository case gets new wording.

Deliberate decisions a reviewer reading only the diff would not know:

  • Both sides of the comparison are physical paths (git already resolves --show-toplevel through symlinks, verified locally), which is what keeps a symlinked clone directory working rather than being mistaken for a nested directory. That is pinned by a test rather than left to trust.
  • The old 'git rev-parse --is-inside-work-tree' check is replaced rather than kept alongside the new one, because it is exactly the check that a nested directory passes; keeping it would add a second, weaker gate for no benefit.
  • The scope is deliberately one guard in one script. bin/fm-ff-lib.sh has a structurally similar upward-discovery check, but fm-fleet-sync.sh does not route through it, so widening this change to that shared library would be unrelated scope.
  • The regression test was written first and confirmed to reproduce the wrong-repo fast-forward against the unfixed script (it reported 'not-a-clone: synced ..' while the enclosing repository moved), then confirmed to pass after the guard. Coverage lands in the existing tests/fm-fleet-sync.test.sh: the whole-fleet form, the single-project form, and the symlinked-clone case.
  • The script header and the test file header were updated so the guard is documented where the behavior lives.

What Changed

  • Require each fleet-sync candidate to match its own physical Git work-tree root, preventing nested directories from updating an enclosing repository under the wrong project label.
  • Report enclosing-repository skips with the repository path while preserving existing non-repository handling and support for symlinked clones, with regression coverage for whole-fleet and single-project syncs.

Risk Assessment

✅ Low: Captain, the targeted root guard closes the reported upward discovery path, preserves intended skip behavior, and the corrected regression fixture now genuinely exercises the pre-fix fast-forward risk.

Testing

The focused regression suite passed, and an end-to-end CLI transcript proves the fixed behavior, preserved cases, and historical failure reproduction. The first transcript command was rejected before execution due to its cleanup syntax, then rerun successfully without worktree changes.

Evidence: Issue #2691 end-to-end CLI transcript

Source: Issue #2691 end-to-end CLI transcript

Scenario 1: whole-fleet refresh with a non-repository directory under projects/
Candidate: /tmp/tmp.AaNgIxTnvm/firstmate-home/projects/worktree-container
Git discovery resolves candidate to enclosing repository: /tmp/tmp.AaNgIxTnvm/firstmate-home
Enclosing HEAD before: ae9b3c45bb5194ea51a345628c508c7d6ba2d6e7
Remote main available: 7db6eeb49d3e005972f9620b2208e1cb534f5890
Fleet-sync output:
worktree-container: skipped: not a clone root (git would act on /tmp/tmp.AaNgIxTnvm/firstmate-home)
Enclosing HEAD after whole-fleet refresh: ae9b3c45bb5194ea51a345628c508c7d6ba2d6e7
Result: enclosing repository unchanged

Scenario 2: direct single-project refresh of the same candidate
Fleet-sync output:
worktree-container: skipped: not a clone root (git would act on /tmp/tmp.AaNgIxTnvm/firstmate-home)
Enclosing HEAD after direct refresh: ae9b3c45bb5194ea51a345628c508c7d6ba2d6e7
Result: enclosing repository unchanged

Scenario 3: symlinked clone remains a valid clone root and fast-forwards
Symlink path physical root: /tmp/tmp.AaNgIxTnvm/real-clone
Git top-level through symlink: /tmp/tmp.AaNgIxTnvm/real-clone
Clone HEAD before: 7db6eeb49d3e005972f9620b2208e1cb534f5890
Remote main available: 366826ed87644834c8d1b469e3bc36e086d85462
Fleet-sync output:
symlinked-clone: synced 7db6eeb..366826e
Clone HEAD after: 366826ed87644834c8d1b469e3bc36e086d85462
Result: symlinked clone advanced to remote main

Scenario 4: a candidate with no discoverable repository keeps the existing wording
Fleet-sync output:
plain-dir: skipped: not a git repo
Result: no-repository case remained a benign skip

Counterfactual: base revision 8714c9a reproduces issue #2691 on the same operator path
Enclosing HEAD before: f3e4c17872baf8187104730d341f84349d239750
Remote main available: 41628a52d20b9e797ba96666750741e631a75c33
Base fleet-sync output:
worktree-container: synced f3e4c17..41628a5
Enclosing HEAD after base whole-fleet refresh: 41628a52d20b9e797ba96666750741e631a75c33
Result: base revision mislabeled the nested directory as synced and fast-forwarded the enclosing repository

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed (2) ✅
  • ⚠️ tests/fm-fleet-sync.test.sh:116 - The regression fixture does not reproduce the reported fast-forward. Its initial commit contains only AGENTS.md, so creating projects/not-a-clone makes the enclosing checkout dirty. Against the pre-fix script, fetch succeeds but status --porcelain sees that untracked directory and returns STUCK instead of fast-forwarding. Track a firstmate-like .gitignore containing /projects/ in the fixture's initial commit so the enclosing checkout remains clean and the before/after SHA assertion proves the dangerous mutation.

🔧 Fix: Confirm clone-root regression fixture remains valid
1 warning still open:

  • ⚠️ tests/fm-fleet-sync.test.sh:116 - The fixture does not reproduce the reported fast-forward. Its initial commit tracks only AGENTS.md, so creating projects/not-a-clone makes the enclosing checkout dirty. Against the pre-fix script, status --porcelain therefore reports STUCK instead of advancing HEAD. Track a .gitignore containing /projects/ in the initial fixture commit so the checkout remains clean and the SHA assertion proves the dangerous mutation.

🔧 Fix: Keep enclosing fixture clean during clone-root regression
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-fleet-sync.test.sh (run twice; both completed successfully)
  • Invoked bin/fm-fleet-sync.sh in whole-fleet and single-project forms against a nested candidate whose enclosing repository had a pending fast-forward; verified the skip output named the enclosing repository and HEAD remained unchanged
  • Invoked bin/fm-fleet-sync.sh symlinked-clone; verified physical and Git top-level paths matched and the clone fast-forwarded
  • Invoked bin/fm-fleet-sync.sh plain-dir outside any repository; verified skipped: not a git repo remained unchanged
  • Executed base revision 8714c9a against the same enclosing-repository fixture; reproduced the mislabeled sync and unintended fast-forward
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 1)
✅ **Push** - passed

✅ No issues found.

Git repository discovery walks upward, so `git -C projects/<dir>` on a plain
directory nested under projects/ resolves to the enclosing repository - in a
firstmate home, the firstmate checkout itself. fm-fleet-sync.sh guarded its
candidates with `rev-parse --is-inside-work-tree`, which such a directory
passes, so every later git call read, pruned and fast-forwarded firstmate's own
default branch and reported it under the project directory's label. A running
session's AGENTS.md changed underneath it, and the report named a project that
had nothing to do with the change.

Require each candidate to be the root of its own work tree before any other git
command: compare `rev-parse --show-toplevel` against the directory's own
physical path. Both sides are physical, so a symlinked clone still compares
equal. Anything else is skipped by name, naming the repository that would have
been touched, and bootstrap relays that as a FLEET_SYNC line.

Regression coverage reproduces the wrong-repo fast-forward against a home nested
inside another repository, in both the whole-fleet and single-project forms, and
pins that a symlinked clone dir still syncs.
@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge, with the clone-root guard preventing the unintended enclosing-repository update while preserving supported synchronization paths.

The changed script rejects candidates whose physical directory is not their resolved Git work-tree root before running subsequent Git operations, and the added tests cover both vulnerable invocation forms plus the symlink compatibility case.

Reviews (1): Last reviewed commit: "no-mistakes(review): Keep enclosing fixt..." | Re-trigger Greptile

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate:

Scheduled 3:10am PT 8/23 pass. Main reconfirmed 8714c9a78c1b4355782fcb9ce1ccf14337478268. VISION.md read in full from that SHA.

VISION (inspected bin/fm-fleet-sync.sh sync_project clone-root guard and tests/fm-fleet-sync.test.sh enclosing-home / symlink / single-project cases). Per-rule: honest interface aligns (skip names the repository git would have touched, instead of a mislabeled project sync); strengthens a refusal path aligns (firstmate checkout is the one tree fleet-sync must never touch); authority / unrequested mutation aligns (/updatefirstmate owns self-update; a nested directory must not do it as a side effect); scripts own the mechanics aligns; restart is a non-event aligns (no mid-session AGENTS.md rewrite from a wrong-repo fast-forward). Scope held to one guard in one script; fm-ff-lib.sh left alone on purpose.

Class: corrective. Replaces --is-inside-work-tree (true for any nested dir inside the home) with a physical toplevel==pwd -P check. Existing skipped: not a git repo wording preserved. Not a new default product path; it refuses a bug.

Security: none. Path comparison plus a louder skip. No .github files. Symlink clones still compare equal via pwd -P / git's resolved toplevel; pinned by test.

Overlap / HOLD: not spawn/teardown, does not edit bin/backends/herdr.sh or fm-spawn.sh. Standing holds #2637/#2692/#2760/#2770/#2622/#2693/#2154/#2586/#2804/#2827/#2829 do not apply as eligibility blockers for this PR. File overlap with hold #2768 on bin/fm-fleet-sync.sh + tests/fm-fleet-sync.test.sh#2768 stays held; this PR is MERGEABLE CLEAN vs current main, so landing the guard would leave the held sibling to rebase later. Not treated as a spawn/teardown sibling of that hold.

CI / NM: HEAD 5f509279db0a624b1b77497f64c6b805d07aa36c. MERGEABLE / CLEAN, ahead 2 / behind 0. Matching no-mistakes-pipeline-attestation:v1 for THIS HEAD. Require no-mistakes SUCCESS (runs 32629198212, 32630050933). CI run 32629198233 all SUCCESS. Greptile SUCCESS — not a gate.

Workflows: already approved (CI completed SUCCESS on this HEAD). Run IDs: 32629198233 (CI), 32629198212 (Require no-mistakes), 32630050933 (Require no-mistakes). No pending first-time-fork approval.

Land-eligible rec: YES (corrective, green CI+NM matching this HEAD, VISION aligns, not default-behavior, not a spawn/teardown/herdr hold). Captain-flag NOW: no.

@kunchenguid
kunchenguid merged commit 801c083 into kunchenguid:main Aug 23, 2026
15 checks passed
@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: this is merged. Thank you @karotkriss — really appreciate you taking the time on this.

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.

fm-fleet-sync silently fast-forwards the firstmate checkout when a non-repo directory sits under projects/

2 participants