Skip to content

feat(bin): let a lone untracked CAPTAINS-LOG.md pass the fleet-sync clean check - #1205

Open
cdonovan-abtex wants to merge 6 commits into
kunchenguid:mainfrom
cdonovan-abtex:fm/firstmate-captains-log-fleet-sync-filter-v1
Open

feat(bin): let a lone untracked CAPTAINS-LOG.md pass the fleet-sync clean check#1205
cdonovan-abtex wants to merge 6 commits into
kunchenguid:mainfrom
cdonovan-abtex:fm/firstmate-captains-log-fleet-sync-filter-v1

Conversation

@cdonovan-abtex

Copy link
Copy Markdown

Intent

Ship the Captain-adopted anchored CAPTAINS-LOG.md fleet-sync eligibility rule in Firstmate shared tracked code. Treat exactly one lone untracked root '?? CAPTAINS-LOG.md' entry as clean only for fleet auto-update, leaving the file untracked, discoverable, and protected by Git from overwrite. Tracked CAPTAINS-LOG modifications, that entry plus any second dirty path, nested logs, and similarly named paths must remain dirty and refuse automatic updating. Do not add any global, repository, or info/exclude ignore and do not weaken general dirty-tree, pruning, worktree, or non-fleet-sync Git safety. Keep the behavior contract in the fleet-sync script header/help rather than AGENTS.md, add colocated shell regression tests, validate relevant runtime/backend axes, and open one PR without merging.

What Changed

  • bin/fm-fleet-sync.sh now treats a working tree whose entire git status --porcelain output is exactly ?? CAPTAINS-LOG.md as eligible for auto-update. The exception is tracked in its own lone_captains_log flag rather than by loosening dirty, so it relaxes only the on-default fast-forward gate — detached-HEAD re-attachment, pruning, and worktree guards still require a fully clean tree, and no ignore rule is added anywhere. A tracked-log modification, the log plus any second dirty path, a nested log, and a similarly named path all stay dirty and refuse to update.
  • When Git's overwrite protection blocks the fast-forward because origin publishes a tracked CAPTAINS-LOG.md, the clone is escalated to a loud, quantified STUCK: branch <default> with untracked CAPTAINS-LOG.md blocking fast-forward instead of a quiet skipped:. Detecting that refusal reads Git's human-facing error text, so the merge --ff-only and the three fetch calls behind the packed-refs-lock recovery are pinned to LC_ALL=C, matching the existing fm-wake-lib.sh idiom; the blocked path is matched literally so a similarly named file cannot satisfy it.
  • The eligibility contract lives in the fm-fleet-sync.sh script header (with the STUCK wording in bootstrap-diagnostics/SKILL.md and docs/architecture.md updated to match), and tests/fm-fleet-sync.test.sh gains 8 colocated regression tests covering the allow case, Git's retained overwrite protection, non-English locale, detached HEAD, and each of the four refuse cases. The suite passes 30/30, including under stock macOS bash 3.2.

Risk Assessment

✅ Low: The final round removes all global shell-mode mutation from the test file in favor of a local || true, restoring the suite's declared set -u-only baseline without altering any assertion, and the production change is a well-bounded, locale-hardened, fully test-pinned eligibility rule that satisfies every stated acceptance criterion.

Testing

I exercised the change both as automated regression tests and as a real end-user CLI run. The colocated suite passes 30/30 under bash 5.3 and again under stock macOS bash 3.2, and fm-test-run.sh --changed --base fa0d85d passes all 8 session-bootstrap suites. For product-level evidence I built a real 7-clone fleet covering every case the intent names and ran the actual bin/fm-fleet-sync.sh: only the clone whose sole dirt is one untracked root CAPTAINS-LOG.md fast-forwarded (log left untracked and unmodified), every other case stayed STUCK and untouched, and the origin-tracks-the-log case surfaced as a quantified STUCK naming the blocking path rather than a benign skip. Running the base-commit script against an identical fleet copy showed all seven as STUCK, isolating the delta. I also verified no ignore of any scope was introduced, that the rule is idempotent on re-run, that prune and worktree safety still behave, and that the session-start fm-bootstrap.sh surface nags for every blocking case while silently updating the blessed one. This is a shell CLI change with no rendered UI, HTML, or renderer surface, so the reviewer-visible evidence is CLI transcripts and post-run Git state — which is precisely what the Captain experiences. No failures, no flakes, worktree left clean.

Evidence: Full evidence transcript (before/after fleet run, per-clone outcomes, Git-protection proof, idempotency, session-start surface, prune safety)
================================================================
 Captain-adopted CAPTAINS-LOG.md fleet-sync eligibility rule
 base fa0d85d  ->  target ac853e7
 Demo fleet: 7 real clones, each in a different state, driven
 through the real bin/fm-fleet-sync.sh CLI.
================================================================

$ # BEFORE (base commit fa0d85d): the same fleet, the old rule
$ fm-fleet-sync.sh          # FM_HOME=<demo fleet>
a-lone-captains-log: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
b-tracked-log-edited: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
c-log-plus-other-dirt: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
d-nested-log: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
e-similar-name: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
f-detached-plus-log: STUCK: on detached HEAD with uncommitted changes, 1 commits behind origin/main - needs attention
g-origin-tracks-log: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention

$ # AFTER (ac853e7): identical fleet, the Captain-adopted rule
$ fm-fleet-sync.sh          # FM_HOME=<demo fleet>
a-lone-captains-log: synced bf9cefc..217e8ec
b-tracked-log-edited: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
c-log-plus-other-dirt: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
d-nested-log: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
e-similar-name: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
f-detached-plus-log: STUCK: on detached HEAD with uncommitted changes, 1 commits behind origin/main - needs attention
g-origin-tracks-log: STUCK: on branch main with untracked CAPTAINS-LOG.md blocking fast-forward, 1 commits behind origin/main - needs attention

----------------------------------------------------------------
 Per-clone outcome
----------------------------------------------------------------
  CLONE                    STATE                                          RESULT
  a-lone-captains-log      lone untracked root ?? CAPTAINS-LOG.md         SYNCED (log kept, untracked)
  b-tracked-log-edited     tracked CAPTAINS-LOG.md modified               STUCK - dirty
  c-log-plus-other-dirt    the log + one more dirty path                  STUCK - dirty
  d-nested-log             notes/CAPTAINS-LOG.md (not root)               STUCK - dirty
  e-similar-name           CAPTAINS-LOG.md.bak                            STUCK - dirty
  f-detached-plus-log      detached HEAD + the lone log                   STUCK - no auto-recovery
  g-origin-tracks-log      origin now TRACKS CAPTAINS-LOG.md              STUCK - Git blocked overwrite

----------------------------------------------------------------

== a-lone-captains-log  (HEAD: fast-forwarded bf9cefc..217e8ec)
$ git status --porcelain
  ?? CAPTAINS-LOG.md
$ cat CAPTAINS-LOG.md          # Captain's local content
  # Captain log
  - day 1: charted the reef
$ git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak   # any ignore rule?
  <none - no ignore rule matches; the log stays visible to Git>
$ ls .git/info/exclude .gitignore 2>&1
  ls: .gitignore: No such file or directory
  .git/info/exclude

== g-origin-tracks-log  (HEAD: unchanged)
$ git status --porcelain
  ?? CAPTAINS-LOG.md
$ cat CAPTAINS-LOG.md          # Captain's local content
  # irreplaceable local log
$ git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak   # any ignore rule?
  <none - no ignore rule matches; the log stays visible to Git>
$ ls .git/info/exclude .gitignore 2>&1
  ls: .gitignore: No such file or directory
  .git/info/exclude

== b-tracked-log-edited  (HEAD: unchanged)
$ git status --porcelain
   M CAPTAINS-LOG.md
$ cat CAPTAINS-LOG.md          # Captain's local content
  upstream log
  local tracked edit
$ git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak   # any ignore rule?
  <none - no ignore rule matches; the log stays visible to Git>
$ ls .git/info/exclude .gitignore 2>&1
  ls: .gitignore: No such file or directory
  .git/info/exclude

== c-log-plus-other-dirt  (HEAD: unchanged)
$ git status --porcelain
  ?? CAPTAINS-LOG.md
  ?? notes.txt
$ cat CAPTAINS-LOG.md          # Captain's local content
  log
$ git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak   # any ignore rule?
  <none - no ignore rule matches; the log stays visible to Git>
$ ls .git/info/exclude .gitignore 2>&1
  ls: .gitignore: No such file or directory
  .git/info/exclude

== d-nested-log  (HEAD: unchanged)
$ git status --porcelain
  ?? notes/
$ git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak   # any ignore rule?
  <none - no ignore rule matches; the log stays visible to Git>
$ ls .git/info/exclude .gitignore 2>&1
  ls: .gitignore: No such file or directory
  .git/info/exclude

== e-similar-name  (HEAD: unchanged)
$ git status --porcelain
  ?? CAPTAINS-LOG.md.bak
$ git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak   # any ignore rule?
  <none - no ignore rule matches; the log stays visible to Git>
$ ls .git/info/exclude .gitignore 2>&1
  ls: .gitignore: No such file or directory
  .git/info/exclude

== f-detached-plus-log  (HEAD: unchanged)
$ git status --porcelain
  ?? CAPTAINS-LOG.md
$ cat CAPTAINS-LOG.md          # Captain's local content
  # Captain log
$ git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak   # any ignore rule?
  <none - no ignore rule matches; the log stays visible to Git>
$ ls .git/info/exclude .gitignore 2>&1
  ls: .gitignore: No such file or directory
  .git/info/exclude

== repo-wide: did fleet-sync write any ignore file anywhere in the fleet?
$ find <fleet> -name .gitignore -o -name exclude | while read f; do echo "$f: $(wc -c <$f) bytes"; done
  /var/folders/l8/htqy6t0j6hd9y084y1jqmqbr0000gn/T//fm-fleet-demo.gw5xPp/projects/f-detached-plus-log/.git/info/exclude: 240 bytes
  /var/folders/l8/htqy6t0j6hd9y084y1jqmqbr0000gn/T//fm-fleet-demo.gw5xPp/projects/d-nested-log/.git/info/exclude: 240 bytes
  /var/folders/l8/htqy6t0j6hd9y084y1jqmqbr0000gn/T//fm-fleet-demo.gw5xPp/projects/a-lone-captains-log/.git/info/exclude: 240 bytes
  /var/folders/l8/htqy6t0j6hd9y084y1jqmqbr0000gn/T//fm-fleet-demo.gw5xPp/projects/c-log-plus-other-dirt/.git/info/exclude: 240 bytes
  /var/folders/l8/htqy6t0j6hd9y084y1jqmqbr0000gn/T//fm-fleet-demo.gw5xPp/projects/e-similar-name/.git/info/exclude: 240 bytes
  /var/folders/l8/htqy6t0j6hd9y084y1jqmqbr0000gn/T//fm-fleet-demo.gw5xPp/projects/g-origin-tracks-log/.git/info/exclude: 240 bytes
  /var/folders/l8/htqy6t0j6hd9y084y1jqmqbr0000gn/T//fm-fleet-demo.gw5xPp/projects/b-tracked-log-edited/.git/info/exclude: 240 bytes
  (0-byte .git/info/exclude files are git's own template; no .gitignore was created)

----------------------------------------------------------------

$ # Every .git/info/exclude in the fleet, stripped of comments/blanks:
  a-lone-captains-log/.git/info/exclude: 0 active rules  (0 mentioning CAPTAINS)
  b-tracked-log-edited/.git/info/exclude: 0 active rules  (0 mentioning CAPTAINS)
  c-log-plus-other-dirt/.git/info/exclude: 0 active rules  (0 mentioning CAPTAINS)
  d-nested-log/.git/info/exclude: 0 active rules  (0 mentioning CAPTAINS)
  e-similar-name/.git/info/exclude: 0 active rules  (0 mentioning CAPTAINS)
  f-detached-plus-log/.git/info/exclude: 0 active rules  (0 mentioning CAPTAINS)
  g-origin-tracks-log/.git/info/exclude: 0 active rules  (0 mentioning CAPTAINS)

$ git config --get core.excludesFile     # any global ignore wired up?
  <unset>

$ # Change footprint of the whole PR:
$ git diff --stat fa0d85d..ac853e7
   bin/fm-fleet-sync.sh        |  49 ++++++++--
   tests/fm-fleet-sync.test.sh | 226 +++++++++++++++++++++++++++++++++++++++++---
   2 files changed, 252 insertions(+), 23 deletions(-)

$ # Idempotency: run fm-fleet-sync.sh a second time on the same fleet
  a-lone-captains-log: already current
  b-tracked-log-edited: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
  c-log-plus-other-dirt: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
  d-nested-log: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
  e-similar-name: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
  f-detached-plus-log: STUCK: on detached HEAD with uncommitted changes, 1 commits behind origin/main - needs attention
  g-origin-tracks-log: STUCK: on branch main with untracked CAPTAINS-LOG.md blocking fast-forward, 1 commits behind origin/main - needs attention

$ # a-lone-captains-log after the second run:
$ git -C projects/a-lone-captains-log status --porcelain
  ?? CAPTAINS-LOG.md
$ cat projects/a-lone-captains-log/CAPTAINS-LOG.md
  # Captain log
  - day 1: charted the reef

----------------------------------------------------------------

$ # What a Captain actually sees at session start (fm-bootstrap relays fleet-sync):
$ fm-bootstrap.sh | grep FLEET_SYNC
FLEET_SYNC: b-tracked-log-edited: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
FLEET_SYNC: c-log-plus-other-dirt: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
FLEET_SYNC: d-nested-log: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
FLEET_SYNC: e-similar-name: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
FLEET_SYNC: f-detached-plus-log: STUCK: on detached HEAD with uncommitted changes, 1 commits behind origin/main - needs attention
FLEET_SYNC: g-origin-tracks-log: STUCK: on branch main with untracked CAPTAINS-LOG.md blocking fast-forward, 1 commits behind origin/main - needs attention

$ # a-lone-captains-log is silently fast-forwarded (no nag), log untouched:
$ git -C projects/a-lone-captains-log status --porcelain && git log --oneline -1
  ?? CAPTAINS-LOG.md
  e623ef2 C1
  -> HEAD == origin/main (fleet auto-update succeeded)

----------------------------------------------------------------

$ # Prune safety is unchanged under the new exception: clone has a lone CAPTAINS-LOG.md,
$ # a 'merged-pr' branch whose remote is gone, and a 'held-by-worktree' branch a worktree still uses.
$ git branch --list        # before
  + held-by-worktree
  * main
    merged-pr
$ fm-fleet-sync.sh prunecase
  prunecase: pruned merged-pr
  prunecase: synced f5d7b9e..6566576
$ git branch --list        # after
  + held-by-worktree
  * main
$ ls <worktree>/           # the worktree-held branch and its checkout survive
  file.txt
$ git status --porcelain   # the Captain's log is still there, untracked
  ?? CAPTAINS-LOG.md
Evidence: Before/after CLI contrast on an identical 7-clone fleet
$ # BEFORE (base fa0d85d): the same fleet, the old rule
$ fm-fleet-sync.sh
a-lone-captains-log: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
b-tracked-log-edited: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
c-log-plus-other-dirt: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
d-nested-log: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
e-similar-name: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
f-detached-plus-log: STUCK: on detached HEAD with uncommitted changes, 1 commits behind origin/main - needs attention
g-origin-tracks-log: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention

$ # AFTER (ac853e7): identical fleet, the Captain-adopted rule
$ fm-fleet-sync.sh
a-lone-captains-log: synced bf9cefc..217e8ec
b-tracked-log-edited: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
c-log-plus-other-dirt: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
d-nested-log: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
e-similar-name: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
f-detached-plus-log: STUCK: on detached HEAD with uncommitted changes, 1 commits behind origin/main - needs attention
g-origin-tracks-log: STUCK: on branch main with untracked CAPTAINS-LOG.md blocking fast-forward, 1 commits behind origin/main - needs attention
Evidence: Post-run state: log stays visible, untracked, and unmodified; no ignore rule anywhere

== a-lone-captains-log (HEAD: fast-forwarded bf9cefc..217e8ec) $ git status --porcelain ?? CAPTAINS-LOG.md $ cat CAPTAINS-LOG.md # Captain's local content # Captain log - day 1: charted the reef $ git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak <none - no ignore rule matches; the log stays visible to Git> == g-origin-tracks-log (HEAD: unchanged) $ git status --porcelain ?? CAPTAINS-LOG.md $ cat CAPTAINS-LOG.md # Captain's local content # irreplaceable local log $ git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak <none - no ignore rule matches; the log stays visible to Git>

== a-lone-captains-log  (HEAD: fast-forwarded bf9cefc..217e8ec)
$ git status --porcelain
  ?? CAPTAINS-LOG.md
$ cat CAPTAINS-LOG.md          # Captain's local content
  # Captain log
  - day 1: charted the reef
$ git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak   # any ignore rule?
  <none - no ignore rule matches; the log stays visible to Git>
$ ls .git/info/exclude .gitignore 2>&1
  ls: .gitignore: No such file or directory
  .git/info/exclude

== g-origin-tracks-log  (HEAD: unchanged)
$ git status --porcelain
  ?? CAPTAINS-LOG.md
$ cat CAPTAINS-LOG.md          # Captain's local content
  # irreplaceable local log
$ git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak   # any ignore rule?
  <none - no ignore rule matches; the log stays visible to Git>
$ ls .git/info/exclude .gitignore 2>&1
  ls: .gitignore: No such file or directory
  .git/info/exclude

== b-tracked-log-edited  (HEAD: unchanged)
$ git status --porcelain
   M CAPTAINS-LOG.md
$ cat CAPTAINS-LOG.md          # Captain's local content
  upstream log
  local tracked edit
$ git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak   # any ignore rule?
  <none - no ignore rule matches; the log stays visible to Git>
$ ls .git/info/exclude .gitignore 2>&1
  ls: .gitignore: No such file or directory
  .git/info/exclude

== c-log-plus-other-dirt  (HEAD: unchanged)
$ git status --porcelain
  ?? CAPTAINS-LOG.md
  ?? notes.txt
$ cat CAPTAINS-LOG.md          # Captain's local content
  log
$ git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak   # any ignore rule?
  <none - no ignore rule matches; the log stays visible to Git>
$ ls .git/info/exclude .gitignore 2>&1
  ls: .gitignore: No such file or directory
  .git/info/exclude

== d-nested-log  (HEAD: unchanged)
$ git status --porcelain
  ?? notes/
$ git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak   # any ignore rule?
  <none - no ignore rule matches; the log stays visible to Git>
$ ls .git/info/exclude .gitignore 2>&1
  ls: .gitignore: No such file or directory
  .git/info/exclude

== e-similar-name  (HEAD: unchanged)
$ git status --porcelain
  ?? CAPTAINS-LOG.md.bak
$ git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak   # any ignore rule?
  <none - no ignore rule matches; the log stays visible to Git>
$ ls .git/info/exclude .gitignore 2>&1
  ls: .gitignore: No such file or directory
  .git/info/exclude

== f-detached-plus-log  (HEAD: unchanged)
$ git status --porcelain
  ?? CAPTAINS-LOG.md
$ cat CAPTAINS-LOG.md          # Captain's local content
  # Captain log
$ git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak   # any ignore rule?
  <none - no ignore rule matches; the log stays visible to Git>
$ ls .git/info/exclude .gitignore 2>&1
  ls: .gitignore: No such file or directory
  .git/info/exclude

== repo-wide: did fleet-sync write any ignore file anywhere in the fleet?
$ find <fleet> -name .gitignore -o -name exclude | while read f; do echo "$f: $(wc -c <$f) bytes"; done
  /var/folders/l8/htqy6t0j6hd9y084y1jqmqbr0000gn/T//fm-fleet-demo.gw5xPp/projects/f-detached-plus-log/.git/info/exclude: 240 bytes
  /var/folders/l8/htqy6t0j6hd9y084y1jqmqbr0000gn/T//fm-fleet-demo.gw5xPp/projects/d-nested-log/.git/info/exclude: 240 bytes
  /var/folders/l8/htqy6t0j6hd9y084y1jqmqbr0000gn/T//fm-fleet-demo.gw5xPp/projects/a-lone-captains-log/.git/info/exclude: 240 bytes
  /var/folders/l8/htqy6t0j6hd9y084y1jqmqbr0000gn/T//fm-fleet-demo.gw5xPp/projects/c-log-plus-other-dirt/.git/info/exclude: 240 bytes
  /var/folders/l8/htqy6t0j6hd9y084y1jqmqbr0000gn/T//fm-fleet-demo.gw5xPp/projects/e-similar-name/.git/info/exclude: 240 bytes
  /var/folders/l8/htqy6t0j6hd9y084y1jqmqbr0000gn/T//fm-fleet-demo.gw5xPp/projects/g-origin-tracks-log/.git/info/exclude: 240 bytes
  /var/folders/l8/htqy6t0j6hd9y084y1jqmqbr0000gn/T//fm-fleet-demo.gw5xPp/projects/b-tracked-log-edited/.git/info/exclude: 240 bytes
  (0-byte .git/info/exclude files are git's own template; no .gitignore was created)
Evidence: What the Captain sees at session start (fm-bootstrap FLEET_SYNC relay)

$ fm-bootstrap.sh | grep FLEET_SYNC FLEET_SYNC: b-tracked-log-edited: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention FLEET_SYNC: c-log-plus-other-dirt: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention FLEET_SYNC: d-nested-log: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention FLEET_SYNC: e-similar-name: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention FLEET_SYNC: f-detached-plus-log: STUCK: on detached HEAD with uncommitted changes, 1 commits behind origin/main - needs attention FLEET_SYNC: g-origin-tracks-log: STUCK: on branch main with untracked CAPTAINS-LOG.md blocking fast-forward, 1 commits behind origin/main - needs attention $ # a-lone-captains-log is silently fast-forwarded (no nag), log untouched: ?? CAPTAINS-LOG.md e623ef2 C1 -> HEAD == origin/main (fleet auto-update succeeded)

$ # What a Captain actually sees at session start (fm-bootstrap relays fleet-sync):
$ fm-bootstrap.sh | grep FLEET_SYNC
FLEET_SYNC: b-tracked-log-edited: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
FLEET_SYNC: c-log-plus-other-dirt: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
FLEET_SYNC: d-nested-log: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
FLEET_SYNC: e-similar-name: STUCK: on branch main with uncommitted changes, 1 commits behind origin/main - needs attention
FLEET_SYNC: f-detached-plus-log: STUCK: on detached HEAD with uncommitted changes, 1 commits behind origin/main - needs attention
FLEET_SYNC: g-origin-tracks-log: STUCK: on branch main with untracked CAPTAINS-LOG.md blocking fast-forward, 1 commits behind origin/main - needs attention

$ # a-lone-captains-log is silently fast-forwarded (no nag), log untouched:
$ git -C projects/a-lone-captains-log status --porcelain && git log --oneline -1
  ?? CAPTAINS-LOG.md
  e623ef2 C1
  -> HEAD == origin/main (fleet auto-update succeeded)
Evidence: Prune and worktree safety unchanged under the new exception

$ git branch --list # before + held-by-worktree * main merged-pr $ fm-fleet-sync.sh prunecase prunecase: pruned merged-pr prunecase: synced f5d7b9e..6566576 $ git branch --list # after + held-by-worktree * main $ git status --porcelain # the Captain's log is still there, untracked ?? CAPTAINS-LOG.md

$ # Prune safety is unchanged under the new exception: clone has a lone CAPTAINS-LOG.md,
$ # a 'merged-pr' branch whose remote is gone, and a 'held-by-worktree' branch a worktree still uses.
$ git branch --list        # before
  + held-by-worktree
  * main
    merged-pr
$ fm-fleet-sync.sh prunecase
  prunecase: pruned merged-pr
  prunecase: synced f5d7b9e..6566576
$ git branch --list        # after
  + held-by-worktree
  * main
$ ls <worktree>/           # the worktree-held branch and its checkout survive
  file.txt
$ git status --porcelain   # the Captain's log is still there, untracked
  ?? CAPTAINS-LOG.md
Evidence: Reproducible demo-fleet builder used to generate the evidence
#!/usr/bin/env bash
# End-to-end demo: build a real 7-clone fleet, each clone in a different state,
# then run the real fm-fleet-sync.sh CLI exactly as a Captain would.
set -eu
ROOT=$1; HOME_DIR=$2; BIN=$3
export GIT_CONFIG_GLOBAL="$HOME_DIR/gitconfig-global"
export GIT_CONFIG_SYSTEM=/dev/null
: > "$GIT_CONFIG_GLOBAL"
git config --global user.name "Captain Test"
git config --global user.email [email protected]
git config --global init.defaultBranch main
git config --global protocol.file.allow always
git config --global advice.detachedHead false

mkdir -p "$HOME_DIR/projects" "$HOME_DIR/remotes"

mk() { # mk <name>  -> origin + clone with one commit on main
  local n=$1; local w="$HOME_DIR/work-$n" r="$HOME_DIR/remotes/$n.git" c="$HOME_DIR/projects/$n"
  git init -q "$w"; git -C "$w" symbolic-ref HEAD refs/heads/main
  printf 'v0\n' > "$w/file.txt"; git -C "$w" add file.txt; git -C "$w" commit -qm C0
  git clone -q --bare "$w" "$r"
  git -C "$w" remote add origin "file://$r"; git -C "$w" push -q -u origin main
  git clone -q "file://$r" "$c"
}
adv() { # adv <name> — one new upstream commit so the clone is 1 behind
  local n=$1; local w="$HOME_DIR/work-$n"
  printf 'v1\n' > "$w/file.txt"; git -C "$w" add file.txt; git -C "$w" commit -qm C1
  git -C "$w" push -q origin main
}

for n in a-lone-captains-log b-tracked-log-edited c-log-plus-other-dirt \
         d-nested-log e-similar-name f-detached-plus-log g-origin-tracks-log; do
  mk "$n"
done

# a: the blessed case — exactly one untracked root CAPTAINS-LOG.md
adv a-lone-captains-log
printf '# Captain log\n- day 1: charted the reef\n' > "$HOME_DIR/projects/a-lone-captains-log/CAPTAINS-LOG.md"

# b: CAPTAINS-LOG.md is TRACKED upstream and locally modified
w=$HOME_DIR/work-b-tracked-log-edited
printf 'upstream log\n' > "$w/CAPTAINS-LOG.md"; git -C "$w" add CAPTAINS-LOG.md
git -C "$w" commit -qm "track captain log"; git -C "$w" push -q origin main
git -C "$HOME_DIR/projects/b-tracked-log-edited" pull -q --ff-only
adv b-tracked-log-edited
printf 'local tracked edit\n' >> "$HOME_DIR/projects/b-tracked-log-edited/CAPTAINS-LOG.md"

# c: the log plus a second dirty path
adv c-log-plus-other-dirt
printf 'log\n'   > "$HOME_DIR/projects/c-log-plus-other-dirt/CAPTAINS-LOG.md"
printf 'scratch\n' > "$HOME_DIR/projects/c-log-plus-other-dirt/notes.txt"

# d: a nested log, not at the repo root
adv d-nested-log
mkdir -p "$HOME_DIR/projects/d-nested-log/notes"
printf 'nested\n' > "$HOME_DIR/projects/d-nested-log/notes/CAPTAINS-LOG.md"

# e: a similarly named path
adv e-similar-name
printf 'backup\n' > "$HOME_DIR/projects/e-similar-name/CAPTAINS-LOG.md.bak"

# f: detached HEAD plus the lone log
adv f-detached-plus-log
git -C "$HOME_DIR/projects/f-detached-plus-log" checkout -q --detach
printf '# Captain log\n' > "$HOME_DIR/projects/f-detached-plus-log/CAPTAINS-LOG.md"

# g: local untracked log, but origin publishes a TRACKED CAPTAINS-LOG.md
printf '# irreplaceable local log\n' > "$HOME_DIR/projects/g-origin-tracks-log/CAPTAINS-LOG.md"
w=$HOME_DIR/work-g-origin-tracks-log
printf 'upstream log\n' > "$w/CAPTAINS-LOG.md"; git -C "$w" add CAPTAINS-LOG.md
git -C "$w" commit -qm "publish captain log"; git -C "$w" push -q origin main

for n in a-lone-captains-log b-tracked-log-edited c-log-plus-other-dirt d-nested-log \
         e-similar-name f-detached-plus-log g-origin-tracks-log; do
  git -C "$HOME_DIR/projects/$n" rev-parse HEAD > "$HOME_DIR/before-$n"
done

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 3 issues found → auto-fixed (3) ✅
  • ⚠️ bin/fm-fleet-sync.sh:341 - The clean-tree exception lets a clone with an untracked root CAPTAINS-LOG.md reach git merge --ff-only even when origin has published a tracked CAPTAINS-LOG.md. Git correctly refuses, but the clone is then reported as skipped: fast-forward failed: ... instead of the previous STUCK: ... N commits behind ... - needs attention. .agents/skills/bootstrap-diagnostics/SKILL.md:32 defines skipped: as "a benign one-off skip (offline, no origin, local-only); bootstrap continued, investigate only if it blocks work", yet this state is permanent and carries no commits-behind count — exactly the "quiet drift" the script header says STUCK exists to prevent. Worse, first_line truncates the merge error to "error: The following untracked working tree files would be overwritten by merge:", dropping the following line that names the file, so the operator cannot tell which path blocked. Consider re-checking for the CAPTAINS-LOG overwrite conflict after a failed fast-forward and routing it through report_stuck (e.g. report_stuck &#34;untracked CAPTAINS-LOG.md blocking fast-forward&#34;) so it stays loud and quantified. tests/fm-fleet-sync.test.sh:314 currently pins the quiet form, so this needs the author's call.
  • ⚠️ bin/fm-fleet-sync.sh:355 - dirty feeds three call sites, not just the fast-forward gate. Relaxing it at line 341 also relaxes the detached-HEAD auto-recovery precondition at line 355, so fleet sync will now run git checkout &lt;default&gt; on a detached clone that holds an untracked root CAPTAINS-LOG.md. That mutates the working tree under a condition that previously required a fully clean tree. It is safe in practice (checkout refuses to overwrite an untracked file when the target branch tracks it, and the failure falls through to report_stuck), but the new header text at lines 12-15 advertises only that "fleet sync may attempt its normal fast-forward" — it does not mention the re-attach path. Either narrow the exception to the fast-forward gate, or extend the header wording to cover recovery and add a regression test for detached-HEAD + lone untracked CAPTAINS-LOG.md, since none of the six new tests exercises that combination.
  • ℹ️ bin/fm-fleet-sync.sh:339 - This repo already has a marker-excluding dirty check with the same shape: dirty_status in bin/fm-ff-lib.sh:225-231 uses awk -v marker=&#34;?? $SUB_HOME_MARKER&#34; &#39;$0 != marker { print; exit }&#39;, which is semantically equivalent, order-independent, streams rather than materializing the whole porcelain output into a shell variable, and preserves the early-exit the removed | head -1 provided. fm-fleet-sync.sh does not source fm-ff-lib.sh (only fm-lock-lib.sh), so reuse is not free and the current inline form is correct as written — noting it only so the two marker-exclusion idioms are a conscious divergence rather than drift.

🔧 Fix: Scope lone-log exception to fast-forward; report blocked sync STUCK
3 issues (1 warning, 2 infos) still open:

  • ⚠️ bin/fm-fleet-sync.sh:149 - is_captains_log_overwrite_refusal matches the English phrase untracked working tree files would be overwritten, but the git merge --ff-only at line 421 runs with the operator's ambient locale. Git ships gettext translations, so under a non-English LANG/LC_ALL the merge refusal is localized, the grep misses, and the clone falls through to echo &#34;$label: skipped: $reason&#34; at line 431 — restoring exactly the benign-skip reporting this commit was written to eliminate, and silently so. This repo already treats locale-dependent command output as a bug and pins it: bin/fm-wake-lib.sh:51 carries the comment "Pin LC_ALL=C so lstart's date format is locale-invariant", with the same idiom at bin/fm-ensure-agents-md.sh:67 and bin/fm-tmux-lib.sh:169. Fix: run the merge as LC_ALL=C git -C &#34;$PROJ&#34; merge --ff-only &#34;$BASE&#34; 2&gt;&amp;1. The pre-existing is_packed_refs_lock_error (line 141) has the identical exposure against the fetches at lines 172/183/205 and is worth pinning in the same pass.
  • ℹ️ bin/fm-fleet-sync.sh:150 - grep -q &#34;^[[:space:]]*$CAPTAINS_LOG\$&#34; interpolates the filename into a basic regular expression, where the . in CAPTAINS-LOG.md is a wildcard — the pattern also matches a blocked path like CAPTAINS-LOGXmd. It is unreachable today because the caller at line 423 requires lone_captains_log = yes, which proves the sole untracked entry is exactly CAPTAINS-LOG.md, so this is a latent trap rather than a live bug. Escaping the literal (grep -q &#39;^[[:space:]]*CAPTAINS-LOG\.md$&#39;) or trimming the leading whitespace and using grep -Fxq -- &#34;$CAPTAINS_LOG&#34; keeps the check correct if the constant or the guard is ever changed.
  • ℹ️ .agents/skills/bootstrap-diagnostics/SKILL.md:34 - The operator-facing STUCK entry enumerates the causes as "the clone is dirty, on a non-default branch, detached with unique commits, or diverged" and does not cover the new branch &lt;default&gt; with untracked CAPTAINS-LOG.md blocking fast-forward state, whose remediation is different (the Captain moves or commits the local log, or upstream stops tracking it — not the usual drift resolution). The STUCK: on &lt;state&gt;, N commits behind &lt;base&gt; - needs attention line format is unchanged, so the doc's parsing guidance and the "dispatch a crewmate" advice still hold. Flagging rather than fixing because your intent requires "Keep the behavior contract in the fleet-sync script header/help rather than AGENTS.md" — this file is neither, so whether the new cause belongs here is your call.

🔧 Fix: Pin LC_ALL=C for git error matching; match log literally
1 warning still open:

  • ⚠️ tests/fm-fleet-sync.test.sh:373 - The new test wraps its guarded run in set +e / set -e, but this file enables only set -u (line 29) — errexit was never on, so the trailing set -e does not restore prior state, it switches errexit on for the rest of the run. The five pre-existing users of this idiom (lines 684, 714, 742, 768, 794) are the last five entries in the invocation list, so their leak was harmless; test_captains_log_stuck_survives_a_non_english_locale is invoked 7th of 30 (line 812), so all 23 subsequent tests now execute under errexit when they were written and validated without it. I scanned those tests and found no command that returns non-zero on the happy path, so the suite should still pass — the cost is latent: any bare grep/git/[ ] later added to a following test will abort the whole run at that point instead of emitting not ok, and the tests after it silently never execute. Change line 373 to set +e to match the file's actual baseline (and consider the same for the five pre-existing sites so the idiom is consistent).

🔧 Fix: Stop test harness from enabling errexit mid-suite
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-fleet-sync.test.sh — 30/30 ok, including the 8 new CAPTAINS-LOG regression tests
  • /bin/bash tests/fm-fleet-sync.test.sh — full suite re-run under stock macOS bash 3.2 (the runtime axis behind issue #1179), exit 0
  • bin/fm-test-run.sh --changed --base fa0d85d — all 8 selected session-bootstrap suites pass (fleet-sync, bootstrap, session-start, sessionstart-nudge, gate-refuse, gotmp, tangle-guard, update)
  • Manual end-to-end: built a 7-clone fleet (lone root log / tracked log edited / log+second dirt / nested log / similar name / detached+log / origin-tracks-log) and ran the real bin/fm-fleet-sync.sh against it — CLI transcript in 02-after-fleet-sync-transcript.txt
  • Manual baseline contrast: ran git show fa0d85d:bin/fm-fleet-sync.sh against an identical copy of the same fleet — all 7 clones reported STUCK, isolating the behavior delta to this change
  • Manual Git-protection proof: post-run git status --porcelain, cat CAPTAINS-LOG.md, git check-ignore -v CAPTAINS-LOG.md notes/CAPTAINS-LOG.md CAPTAINS-LOG.md.bak, git config --get core.excludesFile, and an active-rule count of every .git/info/exclude in the fleet
  • Manual idempotency check: second fm-fleet-sync.sh run on the synced fleet — already current, log still ?? CAPTAINS-LOG.md
  • Manual session-start surface: FM_HOME=&lt;demo fleet&gt; bin/fm-bootstrap.sh | grep FLEET_SYNC — STUCK lines relayed for every blocking case, silence plus a successful fast-forward for the lone-log clone
  • Manual prune/worktree safety: clone with a lone CAPTAINS-LOG plus a gone-upstream merged-pr branch and a worktree-held held-by-worktree branch — fm-fleet-sync.sh prunecase pruned only the gone branch and preserved the worktree
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

main's fbece9c switched fm-teardown to exact tmux selectors and made the
old-vs-new conformance fixture extract bin/backends/tmux.sh from BASE_REF.
On a PR merge ref, merge-base(HEAD, main) is main itself, so the "old"
fixture picked up the exact selectors and the permissive-selector assertion
failed. main's 2459f77 fixes that by resolving a content-historical
permissive adapter; merging main brings the fix onto this branch.
@cdonovan-abtex

Copy link
Copy Markdown
Author

@kunchenguid The refreshed CI and Require no-mistakes workflows are waiting for maintainer approval (action_required); the PR author has read-only access and cannot approve them. Please approve the workflows, then merge/close or leave requested changes once checks finish.

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.

1 participant