Skip to content

feat(bin): replay held local improvements across upstream updates - #1622

Open
JaredHuynhning wants to merge 4 commits into
kunchenguid:mainfrom
JaredHuynhning:fm/fm-local-patch-stack
Open

feat(bin): replay held local improvements across upstream updates#1622
JaredHuynhning wants to merge 4 commits into
kunchenguid:mainfrom
JaredHuynhning:fm/fm-local-patch-stack

Conversation

@JaredHuynhning

Copy link
Copy Markdown

Intent

Let this Firstmate instance take upstream improvements every night while also running local improvements that upstream has not merged. Keep upstream history as a clean, unmodified, fast-forward-only base: do not rewrite or merge into the branch tracking origin. Record held improvements explicitly and inspectably, then transactionally reapply them after every upstream advance. Automatically retire a held improvement when upstream contains content-equivalent work even if it was rebased or squashed, using content rather than commit identity. On a genuine collision, fail closed before publishing anything: keep the primary and linked secondmates on the last known-good effective revision, never silently drop either side, and reuse state/.nightly-update-needs-attention with a message naming the held improvement, the exact upstream change, and affected paths; provide a proven explicit recovery path. Carry the same effective revision into real leased detached linked secondmate worktrees so they cannot silently diverge from the primary, and document any unsupported remote or standalone route honestly. Use bin/fm-push-guard.sh from upstream PR 1602 as the first real held improvement. Prove an upstream advance and held file are both live, prove squash/rebase-equivalent automatic retirement, prove a RED genuine conflict plus recovery, and prove the held file in a real secondmate home. Preserve existing update, fleet-sync, bootstrap, secondmate-sync, test-runner, documentation, and lint behavior.

What Changed

  • Adds an explicit held-improvement patch stack (bin/fm-held-improvements.sh for init/add/retire/list, bin/fm-held-lib.sh for the mechanics) that records each local improvement as an ordered, titled binary patch under config/held-improvements/, keyed by a stable id matched exactly against the <order>-<id> stem.
  • Rewires bin/fm-update.sh so that, when the stack is initialized, the default branch stays a pristine fast-forward of upstream while a scratch worktree rebuilds the full candidate. The primary switches its detached live ref only after every entry applies, entries whose content or patch id already landed upstream retire automatically (rebased and squashed equivalents included, whitespace differences excluded), and a genuine conflict exits nonzero, keeps the prior effective revision live, and writes state/.nightly-update-needs-attention naming the held entry, each colliding upstream commit, and the affected paths (deduplicated per commit). Known-effective refs are capped by HELD_EFFECTIVE_RETAIN so they cannot grow without bound, and init rolls back its directory if the ref or checkout step fails.
  • Teaches bin/fm-ff-lib.sh to resolve the primary's effective commit (default branch, or the held live ref) and to let a clean detached secondmate home switch across replayed ancestry when both ends are known-effective, keeping dirty, branch-checked-out, and unknown-divergence homes untouched. bin/fm-bootstrap.sh now surfaces the attention file as a NIGHTLY_UPDATE_ATTENTION: line on every path including detect-only, bin/fm-test-run.sh routes bin/fm-ff-lib.sh and bin/fm-held-* changes to the secondmate and session-bootstrap suites, and the update, spawn, configuration, architecture, and skill docs are updated to describe held mode, including the unsupported standalone-clone and remote routes.

Risk Assessment

✅ Low: Every accepted round-1 fix landed as a bounded, targeted change with a regression test that is genuinely red against the old code, the held stack remains opt-in local configuration with the default non-held paths untouched, and the two remaining findings are info-level and need either operator misconfiguration of an env knob or roughly ten consecutive failed publishes to bite.

Testing

I ran the new held-improvement suite plus the four adjacent suites that own the preserved update, secondmate-sync, fleet-sync, and bootstrap behaviour, and all pass with no failures. Because passing shell tests alone would not show the product working, I also built a real sandbox (bare origin, primary checkout, and a genuine leased detached linked secondmate worktree) and drove the actual CLIs with the real bin/fm-push-guard.sh pulled from PR 1602 head 8f4c809. The captured transcript shows all four required proofs: an upstream advance and the held file live together on both primary and secondmate at one identical effective revision with the origin-tracking branch left a clean fast-forward (zero merge commits, no held commit on it); a squashed upstream commit with a different commit id retiring the entry by content with a recorded reason; a genuine collision exiting 1, publishing nothing, holding both homes on the last known-good revision, writing state/.nightly-update-needs-attention naming the held id, upstream commit and subject and path, then bootstrap reporting it as NIGHTLY_UPDATE_ATTENTION; and the explicit retire-then-rerun recovery clearing the alarm and reconverging both homes. This change has no UI surface, so the reviewer-visible artifact is the CLI transcript rather than a screenshot. One sub-clause I did not exercise end to end is the held-mode remote-secondmate skip, which needs a configured remote host; it is implemented as an explicit skip message and documented in the updatefirstmate skill and docs/configuration.md.

Evidence: End-to-end held-improvement CLI transcript (upstream advance, squash retirement, RED conflict, recovery, secondmate convergence)

$ bin/fm-held-improvements.sh list active 010 pr-1602 PR 1602 stale-branch push guard $ bin/fm-update.sh # nightly run firstmate: updated eece106..9f5ec53 (upstream main 8d56597..c056223; reapplied: pr-1602; retired: none) secondmate alpha: updated eece106..9f5ec53 (instructions changed: AGENTS.md) $ head -2 sm-alpha/bin/fm-push-guard.sh # the held PR 1602 file is live in the real secondmate home #!/usr/bin/env bash # fm-push-guard.sh - refuse a task-branch push that could discard remote work. $ the branch tracking origin is a clean fast-forward, never merged or rewritten refs/heads/main c056223937b8db42a9e8ff603e959069899c8c53 origin/main c056223937b8db42a9e8ff603e959069899c8c53 strict fast-forward of the recorded base: yes merge commits on the pristine base: 0 held commit present on refs/heads/main: 0 === ACT 3: upstream squashes the same work in; the held entry must retire itself === held source commit ddb84003dcd138634aea8982aef5c99b133a9117 upstream squash 4868d129163762976291f539bca93b74619b2c2e firstmate: updated ebbbe89..4868d12 (upstream main 9dc1066..4868d12; reapplied: none; retired: pr-1602) retired 010 pr-1602 PR 1602 stale-branch push guard upstream content equivalent detected at 4868d129163762976291f539bca93b74619b2c2e === ACT 4: a genuine collision must go RED and publish nothing === --> exit status: 1 $ cat home/state/.nightly-update-needs-attention held improvement pr-1602 (PR 1602 stale-branch push guard) collided with upstream change(s): a2c35070fdcde07c90b498827a3f6e4ce1f288c9 upstream: replace the push guard with an incompatible policy; paths: bin/fm-push-guard.sh; live revision remains 9f5ec53307baa4a2787174f1f5405b8b1b06d710 last known good 9f5ec53307baa4a2787174f1f5405b8b1b06d710 primary HEAD 9f5ec53307baa4a2787174f1f5405b8b1b06d710 sm-alpha HEAD 9f5ec53307baa4a2787174f1f5405b8b1b06d710 NIGHTLY_UPDATE_ATTENTION: held improvement pr-1602 (PR 1602 stale-branch push guard) collided with upstream change(s): a2c35070fdcde07c90b498827a3f6e4ce1f288c9 ... === ACT 5: the documented recovery path === retired: pr-1602 took the upstream push policy after review firstmate: updated 9f5ec53..a2c3507 (upstream main c056223..a2c3507; reapplied: none; retired: none) secondmate alpha: updated 9f5ec53..a2c3507 (instructions changed: bin) --> exit status: 0 (absent - alarm cleared) primary HEAD a2c35070fdcde07c90b498827a3f6e4ce1f288c9 sm-alpha HEAD a2c35070fdcde07c90b498827a3f6e4ce1f288c9


=== SETUP: a sandbox upstream and a primary firstmate checkout, no held stack yet ===

$ git -C main log --oneline -1 && git -C main status -sb
8d56597 upstream: base tree
## main...origin/main

=== ACT 1: record the real PR 1602 push guard as the first held improvement ===

$ bin/fm-held-improvements.sh init main
initialized: upstream=8d56597d310a live=8d56597d310a active=none

$ bin/fm-held-improvements.sh add 010 pr-1602 8d56597 5a71c8a 'PR 1602 stale-branch push guard'
added: pr-1602 PR 1602 stale-branch push guard

$ bin/fm-held-improvements.sh list
active 010 pr-1602 PR 1602 stale-branch push guard

$ ls config/held-improvements/active   # explicit and inspectable on disk
010-pr-1602.patch
010-pr-1602.title

$ bin/fm-update.sh   # first publish of the effective revision
firstmate: updated 8d56597..eece106 (upstream main 8d56597..8d56597; reapplied: pr-1602; retired: none)
reread-firstmate: yes
nudge-secondmates: none

--- lease a REAL detached linked secondmate worktree at the effective revision ---

$ git -C sm-alpha status -sb
## HEAD (no branch)
?? .fm-secondmate-home

=== ACT 2: upstream advances overnight; the held improvement must survive it ===

$ bin/fm-update.sh   # nightly run
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: 2s ago, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
firstmate: updated eece106..9f5ec53 (upstream main 8d56597..c056223; reapplied: pr-1602; retired: none)
secondmate alpha: updated eece106..9f5ec53 (instructions changed: AGENTS.md)
reread-firstmate: yes
nudge-secondmates: fm-alpha

$ grep 'upstream v2' main/README.md          # the upstream advance is live
1:# Firstmate (upstream v2)

$ head -2 main/bin/fm-push-guard.sh          # the held PR 1602 file is live
#!/usr/bin/env bash
# fm-push-guard.sh - refuse a task-branch push that could discard remote work.

$ head -2 sm-alpha/bin/fm-push-guard.sh      # ...and live in the real secondmate home
#!/usr/bin/env bash
# fm-push-guard.sh - refuse a task-branch push that could discard remote work.

$ grep 'upstream v2' sm-alpha/README.md      # the secondmate has the upstream advance too
1:# Firstmate (upstream v2)

$ git -C main log --oneline -3   # pristine upstream base plus the replayed held commit
9f5ec53 held(pr-1602): PR 1602 stale-branch push guard
c056223 upstream: new README section and refreshed instructions
8d56597 upstream: base tree

$ the branch tracking origin is a clean fast-forward, never merged or rewritten
refs/heads/main   c056223937b8db42a9e8ff603e959069899c8c53
origin/main       c056223937b8db42a9e8ff603e959069899c8c53
strict fast-forward of the recorded base: yes
merge commits on the pristine base: 0
held commit present on refs/heads/main: 0

$ effective revision of primary vs secondmate
primary   HEAD 9f5ec53307baa4a2787174f1f5405b8b1b06d710 (detached: yes)
sm-alpha  HEAD 9f5ec53307baa4a2787174f1f5405b8b1b06d710

=== ACT 3: upstream squashes the same work in; the held entry must retire itself ===

$ bin/fm-held-improvements.sh init main && add 010 pr-1602 ... && bin/fm-update.sh
initialized: upstream=9dc106657066 live=9dc106657066 active=none
added: pr-1602 PR 1602 stale-branch push guard
firstmate: updated 9dc1066..ebbbe89 (upstream main 9dc1066..9dc1066; reapplied: pr-1602; retired: none)
reread-firstmate: yes
nudge-secondmates: none

$ the upstream squash commit id differs from the held source commit id
held source commit   ddb84003dcd138634aea8982aef5c99b133a9117
upstream squash      4868d129163762976291f539bca93b74619b2c2e

$ bin/fm-update.sh   # content-equivalent, so commit identity must not matter
firstmate: updated ebbbe89..4868d12 (upstream main 9dc1066..4868d12; reapplied: none; retired: pr-1602)
reread-firstmate: no
nudge-secondmates: none

$ bin/fm-held-improvements.sh list
retired 010 pr-1602 PR 1602 stale-branch push guard

$ cat config/held-improvements/retired/010-pr-1602.reason
upstream content equivalent detected at 4868d129163762976291f539bca93b74619b2c2e

$ head -2 main/bin/fm-push-guard.sh   # the guard is still there, now owned by upstream
#!/usr/bin/env bash
# fm-push-guard.sh - refuse a task-branch push that could discard remote work.

=== ACT 4: a genuine collision must go RED and publish nothing ===

$ bin/fm-update.sh   # expect nonzero and nothing published
WARNING: watcher still down (same stale episode; last beat: 7s ago, grace 300s) - full banner already printed this episode.
error: held improvement pr-1602 (PR 1602 stale-branch push guard) collided with upstream change(s): a2c35070fdcde07c90b498827a3f6e4ce1f288c9 upstream: replace the push guard with an incompatible policy; paths: bin/fm-push-guard.sh; live revision remains 9f5ec53307baa4a2787174f1f5405b8b1b06d710
Performing three-way merge...
Applied patch to 'bin/fm-push-guard.sh' with conflicts.
U bin/fm-push-guard.sh

--> exit status: 1

$ cat home/state/.nightly-update-needs-attention
held improvement pr-1602 (PR 1602 stale-branch push guard) collided with upstream change(s): a2c35070fdcde07c90b498827a3f6e4ce1f288c9 upstream: replace the push guard with an incompatible policy; paths: bin/fm-push-guard.sh; live revision remains 9f5ec53307baa4a2787174f1f5405b8b1b06d710

(colliding upstream commit authored above: a2c35070fdcde07c90b498827a3f6e4ce1f288c9)

$ both homes stayed on the last known-good effective revision
last known good  9f5ec53307baa4a2787174f1f5405b8b1b06d710
primary   HEAD   9f5ec53307baa4a2787174f1f5405b8b1b06d710
sm-alpha  HEAD   9f5ec53307baa4a2787174f1f5405b8b1b06d710

$ head -2 main/bin/fm-push-guard.sh   # the running held side was not dropped
#!/usr/bin/env bash
# fm-push-guard.sh - refuse a task-branch push that could discard remote work.

$ git -C main log --oneline -1 refs/heads/main   # nor was the upstream side
a2c3507 upstream: replace the push guard with an incompatible policy

--- the alarm is not silent: bin/fm-bootstrap.sh reports it at session start ---

$ bin/fm-bootstrap.sh (detect-only) | grep NIGHTLY_UPDATE_ATTENTION
NIGHTLY_UPDATE_ATTENTION: held improvement pr-1602 (PR 1602 stale-branch push guard) collided with upstream change(s): a2c35070fdcde07c90b498827a3f6e4ce1f288c9 upstream: replace the push guard with an incompatible policy; paths: bin/fm-push-guard.sh; live revision remains 9f5ec53307baa4a2787174f1f5405b8b1b06d710

=== ACT 5: the documented recovery path ===

$ bin/fm-held-improvements.sh retire pr-1602 'took the upstream push policy after review'
retired: pr-1602 took the upstream push policy after review

$ bin/fm-update.sh   # rerun after the explicit decision
WARNING: watcher still down (same stale episode; last beat: 9s ago, grace 300s) - full banner already printed this episode.
firstmate: updated 9f5ec53..a2c3507 (upstream main c056223..a2c3507; reapplied: none; retired: none)
secondmate alpha: updated 9f5ec53..a2c3507 (instructions changed: bin)
reread-firstmate: yes
nudge-secondmates: fm-alpha

--> exit status: 0

$ ls home/state/.nightly-update-needs-attention   # a clean run clears the alarm
(absent - alarm cleared)

$ head -3 main/bin/fm-push-guard.sh   # the chosen upstream side is installed
#!/usr/bin/env bash
# upstream replaced the push guard with an incompatible policy
printf "upstream push policy\n"

$ primary and secondmate reconverged
primary   HEAD a2c35070fdcde07c90b498827a3f6e4ce1f288c9
sm-alpha  HEAD a2c35070fdcde07c90b498827a3f6e4ce1f288c9

=== done ===
Evidence: Reproducible end-to-end walkthrough script
#!/usr/bin/env bash
# End-to-end operator walkthrough of the held-improvement stack.
#
# Runs the real bin/fm-held-improvements.sh, bin/fm-update.sh and
# bin/fm-bootstrap.sh from the branch under test against a real sandbox origin,
# a real primary checkout, and a real leased detached linked secondmate
# worktree. The held improvement is the actual bin/fm-push-guard.sh from
# upstream PR 1602 (head 8f4c809).
set -u

REPO=${REPO:?set REPO to the firstmate checkout under test}
EV=${EV:?set EV to the evidence directory}
HELD="$REPO/bin/fm-held-improvements.sh"
UPDATE="$REPO/bin/fm-update.sh"
GUARD="$EV/fm-push-guard-pr1602.sh"
SANDBOX="$EV/e2e"

rm -rf "$SANDBOX"
mkdir -p "$SANDBOX"

export GIT_AUTHOR_NAME='Firstmate E2E' GIT_AUTHOR_EMAIL='e2e@example.invalid'
export GIT_COMMITTER_NAME='Firstmate E2E' GIT_COMMITTER_EMAIL='e2e@example.invalid'
unset GIT_DIR GIT_WORK_TREE
# The sandbox is disposable scratch git, so keep this machine's global commit
# hooks out of the transcript. Nothing here is a real commit.
export GIT_CONFIG_GLOBAL="$SANDBOX/gitconfig"
export GIT_CONFIG_NOSYSTEM=1
printf '[core]\n\thooksPath = /dev/null\n[init]\n\tdefaultBranch = main\n' > "$GIT_CONFIG_GLOBAL"

say() { printf '\n=== %s ===\n' "$*"; }
note() { printf '\n--- %s ---\n' "$*"; }
step() { printf '\n$ %s\n' "$*"; }

HELD_BASE=
HELD_HEAD=

# Build a sandbox world: a bare origin, a seed clone that authors upstream
# commits, and a primary firstmate checkout with the PR 1602 held improvement
# recorded and published.
WORLD=
build_world() {
  local w="$SANDBOX/$1"
  WORLD=$w
  mkdir -p "$w/home/state" "$w/home/data" "$w/home/config"
  touch "$w/home/state/.last-watcher-beat"
  git init -q --bare "$w/origin.git"
  git -C "$w/origin.git" symbolic-ref HEAD refs/heads/main
  git clone -q "$w/origin.git" "$w/seed" 2>/dev/null
  mkdir -p "$w/seed/bin"
  printf 'firstmate instructions v1\n' > "$w/seed/AGENTS.md"
  printf '# Firstmate (upstream v1)\n' > "$w/seed/README.md"
  printf '#!/usr/bin/env bash\nprintf "fleet sync v1\\n"\n' > "$w/seed/bin/fm-fleet-sync.sh"
  chmod +x "$w/seed/bin/fm-fleet-sync.sh"
  git -C "$w/seed" add -A
  git -C "$w/seed" commit -qm 'upstream: base tree'
  git -C "$w/seed" push -q origin main
  git clone -q "$w/origin.git" "$w/main"
  git -C "$w/main" remote set-head origin main >/dev/null 2>&1 || true

  HELD_BASE=$(git -C "$w/seed" rev-parse main)
  git -C "$w/seed" checkout -qb held-pr-1602 main
  install -m 0755 "$GUARD" "$w/seed/bin/fm-push-guard.sh"
  git -C "$w/seed" add bin/fm-push-guard.sh
  git -C "$w/seed" commit -qm 'feat(bin): refuse task-branch pushes that would discard remote work (PR 1602)'
  HELD_HEAD=$(git -C "$w/seed" rev-parse HEAD)
  git -C "$w/seed" push -q origin HEAD:held-pr-1602
  git -C "$w/seed" checkout -q main
  git -C "$w/main" fetch -q origin held-pr-1602
}

build_world world
W=$WORLD
W1602_HEAD=$HELD_HEAD
W1602_BASE=$HELD_BASE

say "SETUP: a sandbox upstream and a primary firstmate checkout, no held stack yet"
step "git -C main log --oneline -1 && git -C main status -sb"
git -C "$W/main" log --oneline -1
git -C "$W/main" status -sb

say "ACT 1: record the real PR 1602 push guard as the first held improvement"
step "bin/fm-held-improvements.sh init main"
FM_ROOT_OVERRIDE="$W/main" FM_HOME="$W/home" "$HELD" init main
step "bin/fm-held-improvements.sh add 010 pr-1602 $(printf '%.7s' "$W1602_BASE") $(printf '%.7s' "$W1602_HEAD") 'PR 1602 stale-branch push guard'"
FM_ROOT_OVERRIDE="$W/main" FM_HOME="$W/home" \
  "$HELD" add 010 pr-1602 "$W1602_BASE" "$W1602_HEAD" 'PR 1602 stale-branch push guard'
step "bin/fm-held-improvements.sh list"
FM_ROOT_OVERRIDE="$W/main" FM_HOME="$W/home" "$HELD" list
step "ls config/held-improvements/active   # explicit and inspectable on disk"
ls -1 "$W/home/config/held-improvements/active"

step "bin/fm-update.sh   # first publish of the effective revision"
FM_ROOT_OVERRIDE="$W/main" FM_HOME="$W/home" "$UPDATE"

note "lease a REAL detached linked secondmate worktree at the effective revision"
git -C "$W/main" worktree add -q --detach "$W/sm-alpha" HEAD
printf 'alpha\n' > "$W/sm-alpha/.fm-secondmate-home"
{ printf 'window=main:fm-alpha\n'; printf 'kind=secondmate\n'; printf 'home=%s/sm-alpha\n' "$W"; } \
  > "$W/home/state/alpha.meta"
step "git -C sm-alpha status -sb"
git -C "$W/sm-alpha" status -sb

say "ACT 2: upstream advances overnight; the held improvement must survive it"
printf '# Firstmate (upstream v2)\n\nUpstream landed a new section tonight.\n' > "$W/seed/README.md"
printf 'firstmate instructions v2\n' > "$W/seed/AGENTS.md"
git -C "$W/seed" add -A
git -C "$W/seed" commit -qm 'upstream: new README section and refreshed instructions'
git -C "$W/seed" push -q origin main

step "bin/fm-update.sh   # nightly run"
FM_ROOT_OVERRIDE="$W/main" FM_HOME="$W/home" "$UPDATE"

step "grep 'upstream v2' main/README.md          # the upstream advance is live"
grep -n 'upstream v2' "$W/main/README.md"
step "head -2 main/bin/fm-push-guard.sh          # the held PR 1602 file is live"
head -2 "$W/main/bin/fm-push-guard.sh"
step "head -2 sm-alpha/bin/fm-push-guard.sh      # ...and live in the real secondmate home"
head -2 "$W/sm-alpha/bin/fm-push-guard.sh"
step "grep 'upstream v2' sm-alpha/README.md      # the secondmate has the upstream advance too"
grep -n 'upstream v2' "$W/sm-alpha/README.md"
step "git -C main log --oneline -3   # pristine upstream base plus the replayed held commit"
git -C "$W/main" log --oneline -3
step "the branch tracking origin is a clean fast-forward, never merged or rewritten"
printf 'refs/heads/main   %s\n' "$(git -C "$W/main" rev-parse refs/heads/main)"
printf 'origin/main       %s\n' "$(git -C "$W/main" rev-parse origin/main)"
printf 'strict fast-forward of the recorded base: %s\n' \
  "$(git -C "$W/main" merge-base --is-ancestor "$W1602_BASE" refs/heads/main && echo yes || echo NO)"
printf 'merge commits on the pristine base: %s\n' \
  "$(git -C "$W/main" rev-list --merges "$W1602_BASE"..refs/heads/main | wc -l | tr -d ' ')"
printf 'held commit present on refs/heads/main: %s\n' \
  "$(git -C "$W/main" log --oneline refs/heads/main | grep -c 'held(pr-1602)')"
step "effective revision of primary vs secondmate"
printf 'primary   HEAD %s (detached: %s)\n' "$(git -C "$W/main" rev-parse HEAD)" \
  "$(git -C "$W/main" symbolic-ref -q HEAD >/dev/null && echo no || echo yes)"
printf 'sm-alpha  HEAD %s\n' "$(git -C "$W/sm-alpha" rev-parse HEAD)"
LAST_GOOD=$(git -C "$W/main" rev-parse HEAD)

say "ACT 3: upstream squashes the same work in; the held entry must retire itself"
build_world squash
SW=$WORLD
step "bin/fm-held-improvements.sh init main && add 010 pr-1602 ... && bin/fm-update.sh"
FM_ROOT_OVERRIDE="$SW/main" FM_HOME="$SW/home" "$HELD" init main
FM_ROOT_OVERRIDE="$SW/main" FM_HOME="$SW/home" \
  "$HELD" add 010 pr-1602 "$HELD_BASE" "$HELD_HEAD" 'PR 1602 stale-branch push guard'
FM_ROOT_OVERRIDE="$SW/main" FM_HOME="$SW/home" "$UPDATE"

install -m 0755 "$GUARD" "$SW/seed/bin/fm-push-guard.sh"
printf '# Firstmate (upstream v2)\n\nUpstream landed a new section tonight.\n' > "$SW/seed/README.md"
git -C "$SW/seed" add bin/fm-push-guard.sh README.md
git -C "$SW/seed" commit -qm 'upstream: squash PR 1602 push guard together with a companion docs change'
SQUASHED=$(git -C "$SW/seed" rev-parse HEAD)
git -C "$SW/seed" push -q origin main
step "the upstream squash commit id differs from the held source commit id"
printf 'held source commit   %s\n' "$HELD_HEAD"
printf 'upstream squash      %s\n' "$SQUASHED"

step "bin/fm-update.sh   # content-equivalent, so commit identity must not matter"
FM_ROOT_OVERRIDE="$SW/main" FM_HOME="$SW/home" "$UPDATE"
step "bin/fm-held-improvements.sh list"
FM_ROOT_OVERRIDE="$SW/main" FM_HOME="$SW/home" "$HELD" list
step "cat config/held-improvements/retired/010-pr-1602.reason"
cat "$SW/home/config/held-improvements/retired/010-pr-1602.reason"
step "head -2 main/bin/fm-push-guard.sh   # the guard is still there, now owned by upstream"
head -2 "$SW/main/bin/fm-push-guard.sh"

say "ACT 4: a genuine collision must go RED and publish nothing"
printf '#!/usr/bin/env bash\n# upstream replaced the push guard with an incompatible policy\nprintf "upstream push policy\\n"\n' \
  > "$W/seed/bin/fm-push-guard.sh"
chmod +x "$W/seed/bin/fm-push-guard.sh"
git -C "$W/seed" add bin/fm-push-guard.sh
git -C "$W/seed" commit -qm 'upstream: replace the push guard with an incompatible policy'
COLLIDER=$(git -C "$W/seed" rev-parse HEAD)
git -C "$W/seed" push -q origin main

step "bin/fm-update.sh   # expect nonzero and nothing published"
FM_ROOT_OVERRIDE="$W/main" FM_HOME="$W/home" "$UPDATE"
printf '\n--> exit status: %s\n' "$?"
step "cat home/state/.nightly-update-needs-attention"
cat "$W/home/state/.nightly-update-needs-attention"
printf '\n(colliding upstream commit authored above: %s)\n' "$COLLIDER"
step "both homes stayed on the last known-good effective revision"
printf 'last known good  %s\n' "$LAST_GOOD"
printf 'primary   HEAD   %s\n' "$(git -C "$W/main" rev-parse HEAD)"
printf 'sm-alpha  HEAD   %s\n' "$(git -C "$W/sm-alpha" rev-parse HEAD)"
step "head -2 main/bin/fm-push-guard.sh   # the running held side was not dropped"
head -2 "$W/main/bin/fm-push-guard.sh"
step "git -C main log --oneline -1 refs/heads/main   # nor was the upstream side"
git -C "$W/main" log --oneline -1 refs/heads/main

note "the alarm is not silent: bin/fm-bootstrap.sh reports it at session start"
step "bin/fm-bootstrap.sh (detect-only) | grep NIGHTLY_UPDATE_ATTENTION"
FM_HOME="$W/home" FM_ROOT_OVERRIDE="$W/main" FM_BOOTSTRAP_DETECT_ONLY=1 \
  "$REPO/bin/fm-bootstrap.sh" 2>/dev/null | grep 'NIGHTLY_UPDATE_ATTENTION' || true

say "ACT 5: the documented recovery path"
step "bin/fm-held-improvements.sh retire pr-1602 'took the upstream push policy after review'"
FM_ROOT_OVERRIDE="$W/main" FM_HOME="$W/home" \
  "$HELD" retire pr-1602 'took the upstream push policy after review'
step "bin/fm-update.sh   # rerun after the explicit decision"
FM_ROOT_OVERRIDE="$W/main" FM_HOME="$W/home" "$UPDATE"
printf '\n--> exit status: %s\n' "$?"
step "ls home/state/.nightly-update-needs-attention   # a clean run clears the alarm"
ls "$W/home/state/.nightly-update-needs-attention" 2>/dev/null || printf '(absent - alarm cleared)\n'
step "head -3 main/bin/fm-push-guard.sh   # the chosen upstream side is installed"
head -3 "$W/main/bin/fm-push-guard.sh"
step "primary and secondmate reconverged"
printf 'primary   HEAD %s\n' "$(git -C "$W/main" rev-parse HEAD)"
printf 'sm-alpha  HEAD %s\n' "$(git -C "$W/sm-alpha" rev-parse HEAD)"

say "done"
Evidence: The real bin/fm-push-guard.sh from PR 1602 head 8f4c809 used as held entry 010-pr-1602
#!/usr/bin/env bash
# fm-push-guard.sh - refuse a task-branch push that could discard remote work.
#
# Usage:
#   fm-push-guard.sh
#   fm-push-guard.sh <remote> <remote-branch>
#
# With no arguments, the current branch's configured upstream is the target.
# The explicit form is for a first push before an upstream exists; both the
# configured remote name and exact remote branch must be supplied. When the
# branch already has an upstream, explicit arguments must name exactly that
# upstream, so a mistyped target cannot clear a branch it never inspected.
#
# The guard contacts the remote immediately before the push and fetches the
# exact target ref into a temporary private ref. It passes when that remote tip
# is an ancestor of HEAD. If histories diverged, it also passes when every
# remote-only single-parent commit has a verbatim patch-equivalent commit in the
# local-only history, which permits a content-preserving rebase or cherry-pick.
# A merge, empty patch, unreachable remote, missing upstream, malformed target,
# a git too old to compute verbatim patch ids, or any other result that cannot
# prove preservation fails closed.
#
# A provably absent remote branch passes only in the explicit two-argument form,
# allowing the first push of a new task branch without treating an offline
# remote as an empty one. This script has no bypass: discarding remote work is a
# captain-level operation and must not be normalized as a worker escape hatch.
set -u
GIT_TERMINAL_PROMPT=0
export GIT_TERMINAL_PROMPT

usage() {
  sed -n '2,26{s/^# \{0,1\}//;p;}' "$0"
}

fail() {
  printf 'error: fm-push-guard: %s\n' "$*" >&2
  exit 1
}

case "${1:-}" in
  -h|--help) usage; exit 0 ;;
esac

case "$#" in
  0|2) ;;
  *) fail "usage: fm-push-guard.sh [<remote> <remote-branch>]" ;;
esac

git rev-parse --git-dir >/dev/null 2>&1 \
  || fail "current directory is not inside a git worktree"

local_branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null) \
  || fail "HEAD is detached; cannot determine the task branch"
configured_remote=$(git config --get "branch.$local_branch.remote" 2>/dev/null || true)
configured_merge=$(git config --get "branch.$local_branch.merge" 2>/dev/null || true)
configured_branch=
if [ -n "$configured_remote" ] || [ -n "$configured_merge" ]; then
  [ -n "$configured_remote" ] && [ -n "$configured_merge" ] \
    || fail "upstream for '$local_branch' is partially configured; repair branch.$local_branch.remote and branch.$local_branch.merge before pushing"
  case "$configured_merge" in
    refs/heads/*) configured_branch=${configured_merge#refs/heads/} ;;
    *) fail "upstream for '$local_branch' is ambiguous: expected refs/heads/*, got '$configured_merge'" ;;
  esac
fi

explicit_target=0
if [ "$#" -eq 2 ]; then
  explicit_target=1
  remote=$1
  remote_branch=${2#refs/heads/}
  if [ -n "$configured_branch" ]; then
    { [ "$remote" = "$configured_remote" ] && [ "$remote_branch" = "$configured_branch" ]; } \
      || fail "explicit target '$remote/$remote_branch' disagrees with the configured upstream '$configured_remote/$configured_branch' of '$local_branch'; the explicit form is only for a first push before an upstream exists"
  fi
else
  [ -n "$configured_branch" ] \
    || fail "branch '$local_branch' has no upstream; name the exact configured remote and remote branch for a first push"
  remote=$configured_remote
  remote_branch=$configured_branch
fi

[ -n "$remote" ] && [ "$remote" != . ] \
  || fail "remote target is ambiguous or local-only: '$remote'"
git remote get-url "$remote" >/dev/null 2>&1 \
  || fail "remote '$remote' is not a configured git remote"
git check-ref-format "refs/heads/$remote_branch" >/dev/null 2>&1 \
  || fail "remote branch '$remote_branch' is not a valid branch name"

target_ref="refs/heads/$remote_branch"
tmp_root=$(mktemp -d "${TMPDIR:-/tmp}/fm-push-guard.XXXXXX") || exit 1
guard_ref="refs/fm-push-guard/$$"
# shellcheck disable=SC2329 # Invoked indirectly by the traps below.
cleanup() {
  git update-ref -d "$guard_ref" >/dev/null 2>&1 || true
  rm -rf "$tmp_root"
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

if git ls-remote --exit-code --heads "$remote" "$target_ref" \
    > "$tmp_root/ls-remote.out" 2> "$tmp_root/ls-remote.err"; then
  :
else
  remote_status=$?
  if [ "$remote_status" -eq 2 ] && [ "$explicit_target" -eq 1 ]; then
    printf "fm-push-guard: safe: remote branch '%s/%s' does not exist; first push may create it.\n" \
      "$remote" "$remote_branch"
    exit 0
  fi
  printf "error: fm-push-guard: cannot determine remote head '%s/%s'; refusing the push.\n" \
    "$remote" "$remote_branch" >&2
  if [ "$remote_status" -eq 2 ]; then
    printf 'error: the configured upstream branch is absent; pass an explicit target only for a deliberate first push.\n' >&2
  else
    sed 's/^/remote: /' "$tmp_root/ls-remote.err" >&2
  fi
  exit 1
fi

line_count=$(wc -l < "$tmp_root/ls-remote.out" | tr -d ' ')
[ "$line_count" = 1 ] \
  || fail "remote head '$remote/$remote_branch' is ambiguous ($line_count matches)"
remote_advertised=$(awk 'NR == 1 { print $1 }' "$tmp_root/ls-remote.out")
[ -n "$remote_advertised" ] \
  || fail "remote head '$remote/$remote_branch' was advertised without an object id"

if ! git fetch --quiet --no-tags "$remote" "+$target_ref:$guard_ref" \
    > "$tmp_root/fetch.out" 2> "$tmp_root/fetch.err"; then
  printf "error: fm-push-guard: cannot fetch remote head '%s/%s'; refusing the push.\n" \
    "$remote" "$remote_branch" >&2
  sed 's/^/remote: /' "$tmp_root/fetch.err" >&2
  exit 1
fi
remote_head=$(git rev-parse --verify "$guard_ref^{commit}" 2>/dev/null) \
  || fail "remote head '$remote/$remote_branch' is not a commit"
[ "$remote_head" = "$remote_advertised" ] \
  || fail "remote head '$remote/$remote_branch' changed while it was being checked; retry immediately before pushing"
local_head=$(git rev-parse --verify 'HEAD^{commit}' 2>/dev/null) \
  || fail "local HEAD is not a commit"

if git merge-base --is-ancestor "$remote_head" "$local_head"; then
  printf "fm-push-guard: safe: '%s/%s' is an ancestor of local HEAD.\n" \
    "$remote" "$remote_branch"
  exit 0
fi

git patch-id --verbatim < /dev/null > /dev/null 2>&1 \
  || fail "this git cannot run 'patch-id --verbatim' (needs git 2.39+); patch equivalence is unprovable, refusing the push to '$remote/$remote_branch'"

local_patches="$tmp_root/local-patches"
: > "$local_patches"
while IFS= read -r commit; do
  [ -n "$commit" ] || continue
  parent_count=$(git rev-list --parents -n 1 "$commit" | awk '{ print NF - 1 }')
  [ "$parent_count" -eq 1 ] || continue
  patch_id=$(git show --no-ext-diff --pretty=format: --binary "$commit" \
    | git patch-id --verbatim | awk 'NR == 1 { print $1 }')
  [ -n "$patch_id" ] && printf '%s\n' "$patch_id" >> "$local_patches"
done < <(git rev-list "$remote_head..$local_head")

unproven="$tmp_root/unproven"
: > "$unproven"
remote_only_count=0
reproduced_count=0
while IFS= read -r commit; do
  [ -n "$commit" ] || continue
  remote_only_count=$((remote_only_count + 1))
  parent_count=$(git rev-list --parents -n 1 "$commit" | awk '{ print NF - 1 }')
  patch_id=
  if [ "$parent_count" -eq 1 ]; then
    patch_id=$(git show --no-ext-diff --pretty=format: --binary "$commit" \
      | git patch-id --verbatim | awk 'NR == 1 { print $1 }')
  fi
  if [ -n "$patch_id" ] && grep -Fqx -- "$patch_id" "$local_patches"; then
    reproduced_count=$((reproduced_count + 1))
  else
    git show -s --format='%H %s' "$commit" >> "$unproven"
  fi
done < <(git rev-list --reverse "$local_head..$remote_head")

if [ ! -s "$unproven" ] && [ "$remote_only_count" -gt 0 ]; then
  printf "fm-push-guard: safe: all %s remote-only commit(s) have patch-equivalent local replacements.\n" \
    "$reproduced_count"
  exit 0
fi

printf "error: fm-push-guard: refusing push to '%s/%s'; local HEAD would lose these remote commits or their preservation cannot be proved:\n" \
  "$remote" "$remote_branch" >&2
sed 's/^/  /' "$unproven" >&2
printf 'error: fetch and fast-forward to the actual PR head before editing; do not reconcile this history by hand.\n' >&2
exit 1
Evidence: Targeted suite results
tests/fm-held-improvements.test.sh 10 ok, 0 not ok
tests/fm-update.test.sh 9 ok, 0 not ok
tests/fm-secondmate-sync.test.sh 20 ok, 0 not ok
tests/fm-fleet-sync.test.sh 22 ok, 0 not ok
tests/fm-bootstrap.test.sh 22 ok, 0 not ok (includes "bootstrap surfaces state/.nightly-update-needs-attention as a detect-only report")

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ⚠️ bin/fm-held-improvements.sh:141 - cmd_retire selects the entry with the unanchored glob &#34;$config&#34;/active/*-&#34;$id&#34;.patch, so an id that is a hyphen-suffix of another entry's id matches the wrong file. With only 010-pr-1602.patch active, fm-held-improvements.sh retire 1602 &#39;&lt;reason&gt;&#39; matches exactly one entry (matches=1 passes) and silently retires pr-1602, moving the wrong held improvement out of the active set on the very path the intent designates as the explicit conflict-recovery route. The same unanchored glob at lines 113 and 116 makes add falsely report id ... is already in use for a genuinely new id. Anchor both to the order prefix, e.g. &#34;$config&#34;/active/[0-9][0-9][0-9]-&#34;$id&#34;.patch.
  • ⚠️ bin/fm-held-lib.sh:157 - The seen dedup in held_conflicting_upstream_changes tests case &#34;\n$seen\n&#34; (literal backslash-n inside double quotes) against an accumulator joined with a real newline (${seen:+$&#39;\n&#39;}), so the pattern can only ever match when seen holds exactly one entry. Reproduced: feeding a, b, a through the identical loop emits emit:a emit:b emit:a. When one upstream commit modifies two or more of a held patch's paths, that commit and subject are repeated in the state/.nightly-update-needs-attention message for every path it touched. Use a real newline in the case subject (case $&#39;\n&#39;&#34;$seen&#34;$&#39;\n&#39; in) or dedup with a seen_&lt;hash&gt; marker.
  • ⚠️ bin/fm-test-run.sh:925 - families_for_changed_path maps bin/fm-ff-lib.sh only to pure-contract-unit, but this change moves held-mode decision logic into that file (primary_head_commit's held_stack_active branch at fm-ff-lib.sh:63 and the effective_rewrite checkout path at fm-ff-lib.sh:349-373). bin/fm-held-* and bin/fm-update.sh were correctly added to the session-bootstrap arm at line 897, but the ff library was not. A later change that touches only bin/fm-ff-lib.sh will therefore not select session-bootstrap, so tests/fm-held-improvements.test.sh, tests/fm-update.test.sh, and tests/fm-bootstrap.test.sh will not run against an edit to the exact code that decides whether a secondmate switches across replayed ancestry. Add session-bootstrap (and secondmate) to the bin/fm-ff-lib.sh arm.
  • ℹ️ bin/fm-held-lib.sh:58 - held_register_effective writes refs/firstmate/held/effective/&lt;sha&gt; and nothing ever prunes it. Every successful held_update_primary adds two refs (the new upstream base and the new candidate), so a nightly updater accumulates roughly 730 refs per year in the primary repo, each one pinning its commit and full tree against git gc. The known-effective set only needs to be deep enough for a linked secondmate that missed a few updates to catch up; consider capping it (retain the last N, or drop entries whose commit is an ancestor of the current pristine base) so the repo does not grow without bound.
  • ℹ️ bin/fm-held-lib.sh:94 - state/.nightly-update-needs-attention has no reader anywhere in this repo: grep across bin/, .agents/, and docs/ finds only this writer, held_clear_attention, the new test, and the two doc sentences. docs/architecture.md:303 describes it as "the existing state/.nightly-update-needs-attention alarm", and the intent says to reuse it, which suggests an external nightly job consumes it. Nothing in the session-start or bearings surface surfaces it, so within this repo a replay conflict is visible only through fm-update.sh's nonzero exit and stderr. Please confirm the external consumer exists, or the conflict may go unnoticed by a captain who does not read the nightly job's output.
  • ℹ️ bin/fm-held-improvements.sh:94 - cmd_init does mv &#34;$tmp&#34; &#34;$config&#34; before git update-ref &#34;$HELD_LIVE_REF&#34; and the detaching checkout, and the script runs under set -eu. If either later step fails, config/held-improvements/ exists while the live ref does not. From that state held_stack_present is true so every fm-update.sh run fails closed with an attention alarm, while init refuses with "already exists" and add/retire/list refuse with "held-improvement stack is not initialized". There is no supported command to get out, so the operator must hand-remove the directory, which is at odds with the intent's requirement for a proven explicit recovery path. Either publish the ref before the directory is moved into place, or give the tool a deinit/repair path.
  • ℹ️ tests/fm-held-improvements.test.sh:21 - The intent names "bin/fm-push-guard.sh from upstream PR 1602 as the first real held improvement". The committed evidence uses a two-line stand-in (printf &#34;push guard from PR 1602\\n&#34;) synthesised inside the test, and the real stack lives under gitignored config/held-improvements/, so nothing in the diff shows PR 1602's actual patch was captured. The test proves the machinery end to end, which is the right thing for a test to do, but it does not prove the real held improvement is live. Please confirm the actual PR 1602 patch was captured into the local stack, since that part is operator-local state a source review cannot verify.

🔧 Fix: fix held stack id matching, dedup, ref growth, init atomicity
2 infos still open:

  • ℹ️ bin/fm-held-lib.sh:356 - held_update_primary registers $new_base and $candidate as known-effective at lines 356-357, before git checkout --detach at 358 and before the live ref publishes at 364. A run that fails after that point therefore still consumes two slots of the bounded window without publishing anything. Combined with HELD_EFFECTIVE_RETAIN having no floor at line 64, the previous live commit's anchor can be evicted: the ordering is [.., old_live, new_base, candidate], so it survives only while the retain value is at least 3, and roughly ten consecutive failed publishes at the default of 20 push it out too. Once that ref is gone, held_commit_is_known_effective returns false for every linked secondmate still sitting on old_live, the ancestry check also fails across the replay, and ff_target reports skipped: diverged for each home with no supported command to re-anchor them. The failure is reported rather than silent, and bootstrap now surfaces the nightly alarm, so an operator would normally act first. Move the two held_register_effective calls below the successful update-ref &#34;$HELD_LIVE_REF&#34; so only published revisions consume the window, and clamp HELD_EFFECTIVE_RETAIN to a minimum of 3 since docs/configuration.md:230 advertises the knob without stating a lower bound.
  • ℹ️ bin/fm-bootstrap.sh:1025 - The while IFS= read -r attention_line ... done &lt; &#34;$nightly_attention&#34; loop discards a final line that has no trailing newline, because read returns nonzero on EOF even after assigning the partial line. held_attention writes through held_write_file, which always terminates with printf &#39;%s\n&#39;, so this is defensive rather than currently reachable. It matters because the alarm message puts the multi-line upstream commit list in the middle and the paths and live-revision tail on the last line, so a truncated write would silently cost the operator exactly the part that names the affected paths. Change the loop condition to while IFS= read -r attention_line || [ -n &#34;$attention_line&#34; ]; do.
✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-held-improvements.test.sh (10 cases: convergence, content-equivalent retirement, whitespace non-equivalence, relative FM_HOME, checked-out pristine base refusal, RED conflict plus recovery, exact id matching, collision dedup, init rollback, bounded effective refs)
  • bash tests/fm-update.test.sh (preserves the non-held fast-forward update behaviour)
  • bash tests/fm-secondmate-sync.test.sh (preserves spawn/bootstrap secondmate sync behaviour after the fm-ff-lib.sh change)
  • bash tests/fm-fleet-sync.test.sh (preserves fleet-sync behaviour)
  • bash tests/fm-bootstrap.test.sh (includes the new detect-only NIGHTLY_UPDATE_ATTENTION report)
  • Manual end-to-end: REPO=$(pwd) EV=... bash held-improvements-e2e.sh driving the real bin/fm-held-improvements.sh init/add/list/retire, bin/fm-update.sh, and bin/fm-bootstrap.sh against a sandbox origin, a primary checkout, and a real git worktree add --detach secondmate home
  • Fetched the actual bin/fm-push-guard.sh from PR 1602 head 8f4c809 via gh api and used it verbatim as held entry 010-pr-1602
⚠️ **Document** - 1 info
  • ℹ️ bin/fm-held-improvements.sh:152 - bin/fm-lint.sh fails on this branch with SC1007 at bin/fm-held-improvements.sh:152 (local id=$1 reason=$2 config patch found= stem matches=0). This is executable code introduced by the change, outside the documentation phase's remit, so it was left for the lint phase.
🔧 **Lint** - 1 issue found → auto-fixed ✅
  • ⚠️ linter found issues (exit code 1)

🔧 Fix: make empty found init explicit in fm-held-improvements.sh
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

@JaredHuynhning
JaredHuynhning force-pushed the fm/fm-local-patch-stack branch from b022914 to 1646067 Compare August 7, 2026 03:24
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