Skip to content

feat: sync Firstmate landings to the configured fork - #33

Merged
BohnBawerick merged 5 commits into
mainfrom
fm/fm-landing-pushes-fork
Oct 4, 2026
Merged

BohnBawerick merged 5 commits into
mainfrom
fm/fm-landing-pushes-fork

Conversation

@BohnBawerick

Copy link
Copy Markdown
Owner

Intent

Captain's intent

"wait why the fuck our local setup isnt upload to the fucking gothub ?? whatelse is the fucning point of having github at all ?our own work needs to fucking go there like it is our fucking fork for crist sake isnt that the entire point ??? can you please after that get it all sync eoth our fork and why dont we already sne doit general fizes back to the original project (i thought we did that already) and make it routine hes every week sure we do the updates then we sync it all eith our fork so we keep our setups and all our unique changes are both sent to the og and to our own fucking fork which exists exactt for this fucking reason"

Context, 4 Oct 2026: approved firstmate work lands on this home's local main through bin/fm-merge-local.sh, and the PR on our fork https://github.com/BohnBawerick/firstmate stays open by design, so nothing pushed local main to GitHub. By 4 Oct our fork's main was 24 commits behind the local main this home runs. That broke the remote second mates' sync, because they fetch from the fork, and it left PRs that had really landed showing as open. Firstmate pushed by hand twice that day as clean fast-forwards (71bd89c..1726136, then ..44647b7), and GitHub then showed those PRs as merged. The weekly upstream merge and sending general fixes to the original project are a separate routine task; this task is only about our own fork.

Firstmate spec

Load the firstmate-coding-guidelines skill before editing anything.

Make every approved firstmate landing also update our fork, so GitHub always holds what this home runs:

  1. After bin/fm-merge-local.sh fast-forwards local main for this home's own firstmate repository, push local main to the landing remote's main (origin, per bin/fm-landing-remote.sh) as a plain fast-forward. Never force, never create a merge commit, and never push anywhere else.
  2. Confirm the push landed. When the task has a recorded PR, read back that GitHub shows it merged, the same way bin/fm-pr-merge.sh proves a merge.
  3. If the push cannot be a fast-forward, or it fails, the local landing stays as it is. The script reports plainly that the fork was not updated, says why, and gives the exact command to finish it, then exits non-zero. It must never report a landed-and-synced result it did not prove.
  4. This applies only to this home's own firstmate repository. Local-only project landings keep today's behavior, since they have no remote by design.
  5. Update every place that says the outward PR "remains open" to the new behavior, including AGENTS.md section 1, docs/configuration.md, and any skill or doc that repeats it. Keep the wording one owner per contract, per the coding guidelines.
  6. Add tests next to the existing fm-merge-local tests for the fast-forward push, the refusal when it is not a fast-forward, a push failure, and the merged read-back.

Out of scope: the weekly upstream merge, upstream PRs, and changes to project landings.

What Changed

  • Push approved Firstmate landings to the verified fork as plain fast-forwards, then read back the remote branch. Failed or unsafe syncs keep the local landing and exit with a recovery command.
  • Confirm recorded GitHub pull requests report as merged after syncing, and refuse Firstmate self-repository tasks in the forge merge path.
  • Update the landing documentation and add tests for successful syncs, divergence, push failures, remote validation, and pull request read-back.

Risk Assessment

✅ Low: Captain, the change is bounded to Firstmate self-repository landings, validates the fork destination before pushing, preserves local landings on synchronization failure, and adds behavior-level regression coverage.

Testing

Ran the focused fm-merge-local behavior suite, then drove eight isolated live scenarios through the real landing and PR-merge scripts using disposable Git remotes. The happy path also performed a normal read-only GitHub GraphQL check of merged PR 32. All scenarios passed, the fixtures were removed, and the worktree is clean. These are CLI changes with no rendered UI, so the reviewer evidence is a CLI transcript.

  • Live validation: ✅ go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
An approved Firstmate landing fast-forwards local main, pushes the proven fork, reads the remote tip back, and verifies the recorded GitHub PR as merged ✅ pass live Live fork-sync product transcript
A diverged fork remains untouched while the local landing remains, and the command exits 3 with the reason and exact finishing command ✅ pass live Live fork-sync product transcript
A remote push rejection leaves the fork untouched and reports the remote error without undoing the local landing ✅ pass live Live fork-sync product transcript
A mismatched origin push URL is refused before either the configured fork or upstream remote moves ✅ pass live Live fork-sync product transcript
A checkout with no upstream remote lands locally, skips the push, and says that no fork is configured ✅ pass live Live fork-sync product transcript
A non-GitHub recorded PR gets a proved fork update and an explicit notice that PR state was not checked ✅ pass live Live fork-sync product transcript
The forge merge command refuses Firstmate's local-authoritative repository before calling gh or changing main or PR metadata ✅ pass live Live fork-sync product transcript
If the task record is replaced after the landing lock is released, fork sync still checks the PR captured from the locked original record ✅ pass live Live fork-sync product transcript
Evidence: Live fork-sync product transcript

Source: Live fork-sync product transcript

Firstmate fork-sync live validation
target=44276faef2b3dbeed3291d9594122f731fe56596
merged_pr=https://github.com/BohnBawerick/firstmate/pull/32

[approved landing pushes the fork and reads a real GitHub PR as merged]
exit=0
stdout:
merged fm/task-happy into local main (8036fa1 -> f168826) in ~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/happy/firstmate
pushed local main to origin/main (8036fa14 -> f168826e)
verified: https://github.com/BohnBawerick/firstmate/pull/32 is merged
stderr:
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, 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.
●  repair missing watcher supervision with a foreground checkpoint: bin/fm-watch-checkpoint.sh --seconds 180.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
local_main=f168826e8ed174d49c8635bc439dd1d5279f2715
origin_main=f168826e8ed174d49c8635bc439dd1d5279f2715
task_head=f168826e8ed174d49c8635bc439dd1d5279f2715

[diverged fork is refused after preserving the local landing]
exit=3
stdout:
merged fm/task-diverged into local main (48a27a5 -> 1abef74) in ~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/diverged/firstmate
stderr:
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, 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.
●  repair missing watcher supervision with a foreground checkpoint: bin/fm-watch-checkpoint.sh --seconds 180.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
fork not updated: local main landed at 1abef74, but origin/main was not updated: it is not a fast-forward: origin/main is at 17d3f96e, which local main does not contain
See the missing commits with: git -C ~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/diverged/firstmate log --oneline 1abef7472f5dea0b12f5e3f3f6874bf413c68da5..17d3f96e0cf6dfc2cc0db602b223c3dd2049424d
Bring them into local main through a reviewed task, then finish with: git -C ~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/diverged/firstmate push origin refs/heads/main:refs/heads/main
local_main=1abef7472f5dea0b12f5e3f3f6874bf413c68da5
origin_main=17d3f96e0cf6dfc2cc0db602b223c3dd2049424d
task_head=1abef7472f5dea0b12f5e3f3f6874bf413c68da5

[remote rejection is reported and the local landing remains]
exit=3
stdout:
merged fm/task-rejected into local main (48a27a5 -> c0ec5e0) in ~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/rejected/firstmate
stderr:
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, 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.
●  repair missing watcher supervision with a foreground checkpoint: bin/fm-watch-checkpoint.sh --seconds 180.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
fork not updated: local main landed at c0ec5e0, but origin/main was not updated: the push failed: remote: remote policy rejected this push        
To file://~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/rejected/origin.git
 ! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to 'file://~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/rejected/origin.git'
Finish with: git -C ~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/rejected/firstmate push origin refs/heads/main:refs/heads/main
local_main=c0ec5e0fc4b2cbd1a0524d1833440f8781272a00
origin_main=48a27a540f5fea5983fd59e0b8a179aa61c20a13
task_head=c0ec5e0fc4b2cbd1a0524d1833440f8781272a00

[mismatched push URL is refused before either remote moves]
exit=3
stdout:
merged fm/task-unsafe into local main (4272319 -> 6182513) in ~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/unsafe/firstmate
stderr:
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, 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.
●  repair missing watcher supervision with a foreground checkpoint: bin/fm-watch-checkpoint.sh --seconds 180.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
fork not updated: local main landed at 6182513, but origin/main was not updated: origin would push to file://~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/unsafe/upstream.git rather than its verified fetch URL: origin is file://~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/unsafe/origin.git, not the landing remote file://~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/unsafe/upstream.git
Remove or correct remote.origin.pushurl, then finish with: git -C ~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/unsafe/firstmate push origin refs/heads/main:refs/heads/main
local_main=618251315e7349ea0075ba303a16c976801c87e1
origin_main=427231927bb786a81a218005dfd40ca8e69cd8f8
task_head=618251315e7349ea0075ba303a16c976801c87e1

[checkout without an upstream remote skips fork sync plainly]
exit=0
stdout:
merged fm/task-nofork into local main (4272319 -> fe66579) in ~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/nofork/firstmate
no fork is configured in ~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/nofork/firstmate because there is no upstream remote; nothing was pushed
stderr:
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, 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.
●  repair missing watcher supervision with a foreground checkpoint: bin/fm-watch-checkpoint.sh --seconds 180.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
local_main=fe66579fa82b9f464e9dcc3938f6941e4fd480f0
origin_main=427231927bb786a81a218005dfd40ca8e69cd8f8
task_head=fe66579fa82b9f464e9dcc3938f6941e4fd480f0

[non-GitHub PR still gets branch proof and an explicit read-back skip]
exit=0
stdout:
merged fm/task-nongithub into local main (cd2b11d -> 318c3ac) in ~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/nongithub/firstmate
pushed local main to origin/main (cd2b11df -> 318c3ac5)
origin/main now holds local main; PR state was not checked because 'https://gitlab.example.com/group/firstmate/-/merge_requests/9' is not a GitHub pull request
stderr:
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, 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.
●  repair missing watcher supervision with a foreground checkpoint: bin/fm-watch-checkpoint.sh --seconds 180.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
local_main=318c3ac511b9bd867954bd7cee939038fcd858fe
origin_main=318c3ac511b9bd867954bd7cee939038fcd858fe
task_head=318c3ac511b9bd867954bd7cee939038fcd858fe

[forge merge path refuses the local-authoritative repository before gh]
exit=1
stdout:
stderr:
error: task task-prguard is Firstmate's local-authoritative repository; use bin/fm-merge-local.sh task-prguard after approval instead of merging its PR
local_main=cd2b11dfc657115950ab72b0d8506c95d1fd84ab
origin_main=cd2b11dfc657115950ab72b0d8506c95d1fd84ab
task_head=eb129b280552a534324b4e986f8900a81369b094

[task reuse during fork sync cannot replace the locked PR snapshot]
exit=0
stdout:
merged fm/task-snapshot into local main (cd2b11d -> 4a0f692) in ~/.no-mistakes/worktrees/842a31c92d33/01M42A65ET1EDVWST0EP8R8HGB/.live-fork-validation.2200351/snapshot/firstmate
pushed local main to origin/main (cd2b11df -> 4a0f6921)
verified: https://github.com/BohnBawerick/firstmate/pull/32 is merged
stderr:
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, 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.
●  repair missing watcher supervision with a foreground checkpoint: bin/fm-watch-checkpoint.sh --seconds 180.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
local_main=4a0f692116d0d1817d6b39e8c5105a72cbd6e95a
origin_main=4a0f692116d0d1817d6b39e8c5105a72cbd6e95a
task_head=4a0f692116d0d1817d6b39e8c5105a72cbd6e95a

RESULT: all live scenarios passed

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 4 issues found → auto-fixed (3) ✅
  • 🚨 bin/fm-merge-local.sh:205 - The new push does not prove its destination. fm-landing-remote.sh verify succeeds for any single-remote clone and checks only origin's fetch URL, while git push origin uses any configured remote.origin.pushurl. A clone whose sole origin is the parent, or a correctly remapped clone with a stale parent pushurl, reaches line 229 and can push to the parent despite the explicit "never push anywhere else" requirement. The reads at lines 209 and 233 still inspect the fetch URL, so the latter case can mutate the parent before failing read-back. Require a proven remapped origin and verify every push URL before pushing. The positive fixture at tests/fm-merge-local.test.sh:510 uses the unproved single-origin shape, while its refusal case at line 690 covers only a leftover fork remote.
  • 🚨 bin/fm-merge-local.sh:199 - The explicit fallback for a missing origin contradicts requirements 1 and 3: every approved Firstmate landing must update the fork, and an update failure must report that the fork was not updated and exit non-zero. A self-repository task can fast-forward local main, then lines 199-201 skip both fork sync and PR read-back while returning success. Remove this unrequired no-fork success mode and route it through the existing non-zero fork_not_synced path.
  • ⚠️ bin/fm-merge-local.sh:261 - The GitLab read-back branch adds a second provider contract that the stated GitHub-fork requirement does not need. Narrow this path to the required GitHub PR form and reject other providers instead of maintaining an unused GitLab mode.
  • ⚠️ bin/fm-merge-local.sh:252 - FM_MERGE_LOCAL_READBACK_DELAY introduces a new operator-configurable option, validation rule, and documented contract, although the intent only requires bounded read-back confirmation. Remove the option and keep the delay internal; tests can replace sleep through their executable test environment if they need zero-delay retries.

🔧 Fix applied.
6 issues (4 errors, 2 warnings) still open:

  • 🚨 bin/fm-merge-local.sh:205 - The new push does not prove its destination. fm-landing-remote.sh verify succeeds for any single-remote clone and checks only origin's fetch URL, while git push origin uses any configured remote.origin.pushurl. A clone whose sole origin is the parent, or a correctly remapped clone with a stale parent pushurl, reaches line 229 and can push to the parent despite the explicit "never push anywhere else" requirement. The reads at lines 209 and 233 still inspect the fetch URL, so the latter case can mutate the parent before failing read-back. Require a proven remapped origin and verify every push URL before pushing. The positive fixture at tests/fm-merge-local.test.sh:510 uses the unproved single-origin shape, while its refusal case at line 690 covers only a leftover fork remote.
  • 🚨 bin/fm-merge-local.sh:199 - The explicit fallback for a missing origin contradicts requirements 1 and 3: every approved Firstmate landing must update the fork, and an update failure must report that the fork was not updated and exit non-zero. A self-repository task can fast-forward local main, then lines 199-201 skip both fork sync and PR read-back while returning success. Remove this unrequired no-fork success mode and route it through the existing non-zero fork_not_synced path.
  • ⚠️ bin/fm-merge-local.sh:261 - The GitLab read-back branch adds a second provider contract that the stated GitHub-fork requirement does not need. Narrow this path to the required GitHub PR form and reject other providers instead of maintaining an unused GitLab mode.
  • ⚠️ bin/fm-merge-local.sh:252 - FM_MERGE_LOCAL_READBACK_DELAY introduces a new operator-configurable option, validation rule, and documented contract, although the intent only requires bounded read-back confirmation. Remove the option and keep the delay internal; tests can replace sleep through their executable test environment if they need zero-delay retries.
  • 🚨 bin/fm-merge-local.sh:237 - The new fork synchronization breaks the supported self-repository path at bin/fm-pr-merge.sh:1499. That path merges the GitHub PR first, then calls this script. With GitHub's default squash merge, origin/main contains a new squash commit while the local task branch contains the original commits. Lines 231-240 fast-forward local main to the task branch and then reject origin/main as divergent, leaving local and remote main split after the irreversible forge merge. The existing test misses this because its fixture has no upstream, so the fix-round fallback at line 199 exits successfully without syncing. The prior fix round left this sibling caller behind. The smallest containment is to refuse self-repository use in fm-pr-merge.sh before the forge mutation and direct callers to fm-merge-local.sh. Preserving the existing entry point requires a new reconciliation policy, so that product choice needs authorization.
  • 🚨 bin/fm-merge-local.sh:199 - The prior fix round tests whether git remote get-url upstream succeeds instead of whether the upstream remote exists. A remapped checkout whose remote.upstream.url was removed but whose remote section remains, for example through remote.upstream.fetch, reaches this branch after the local landing, reports that no fork is configured, and exits 0 without syncing. This is the malformed-remap sibling that the missing-origin case did not cover. Check for the remote name separately, then route an unreadable upstream URL through fork_not_synced.

🔧 Fix applied.
7 issues (5 errors, 2 warnings) still open:

  • 🚨 bin/fm-merge-local.sh:205 - The new push does not prove its destination. fm-landing-remote.sh verify succeeds for any single-remote clone and checks only origin's fetch URL, while git push origin uses any configured remote.origin.pushurl. A clone whose sole origin is the parent, or a correctly remapped clone with a stale parent pushurl, reaches line 229 and can push to the parent despite the explicit "never push anywhere else" requirement. The reads at lines 209 and 233 still inspect the fetch URL, so the latter case can mutate the parent before failing read-back. Require a proven remapped origin and verify every push URL before pushing. The positive fixture at tests/fm-merge-local.test.sh:510 uses the unproved single-origin shape, while its refusal case at line 690 covers only a leftover fork remote.
  • 🚨 bin/fm-merge-local.sh:199 - The explicit fallback for a missing origin contradicts requirements 1 and 3: every approved Firstmate landing must update the fork, and an update failure must report that the fork was not updated and exit non-zero. A self-repository task can fast-forward local main, then lines 199-201 skip both fork sync and PR read-back while returning success. Remove this unrequired no-fork success mode and route it through the existing non-zero fork_not_synced path.
  • ⚠️ bin/fm-merge-local.sh:261 - The GitLab read-back branch adds a second provider contract that the stated GitHub-fork requirement does not need. Narrow this path to the required GitHub PR form and reject other providers instead of maintaining an unused GitLab mode.
  • ⚠️ bin/fm-merge-local.sh:252 - FM_MERGE_LOCAL_READBACK_DELAY introduces a new operator-configurable option, validation rule, and documented contract, although the intent only requires bounded read-back confirmation. Remove the option and keep the delay internal; tests can replace sleep through their executable test environment if they need zero-delay retries.
  • 🚨 bin/fm-merge-local.sh:237 - The new fork synchronization breaks the supported self-repository path at bin/fm-pr-merge.sh:1499. That path merges the GitHub PR first, then calls this script. With GitHub's default squash merge, origin/main contains a new squash commit while the local task branch contains the original commits. Lines 231-240 fast-forward local main to the task branch and then reject origin/main as divergent, leaving local and remote main split after the irreversible forge merge. The existing test misses this because its fixture has no upstream, so the fix-round fallback at line 199 exits successfully without syncing. The prior fix round left this sibling caller behind. The smallest containment is to refuse self-repository use in fm-pr-merge.sh before the forge mutation and direct callers to fm-merge-local.sh. Preserving the existing entry point requires a new reconciliation policy, so that product choice needs authorization.
  • 🚨 bin/fm-merge-local.sh:199 - The prior fix round tests whether git remote get-url upstream succeeds instead of whether the upstream remote exists. A remapped checkout whose remote.upstream.url was removed but whose remote section remains, for example through remote.upstream.fetch, reaches this branch after the local landing, reports that no fork is configured, and exits 0 without syncing. This is the malformed-remap sibling that the missing-origin case did not cover. Check for the remote name separately, then route an unreadable upstream URL through fork_not_synced.
  • 🚨 bin/fm-merge-local.sh:261 - After the task control lock is released at line 170, this rereads pr= from mutable task metadata. Ordinary landed-work teardown uses the same control lock and may then remove the record, so a slow fork push can reach line 262 with an empty PR_URL and exit 0 without proving the original recorded GitHub PR merged. ID reuse can instead make it inspect another incarnation's PR. Round 2 left this lifecycle sibling behind. Snapshot the validated pr= for the expected spawn generation before releasing the control lock, then use that snapshot for read-back.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
An approved Firstmate landing fast-forwards local main, pushes the proven fork, reads the remote tip back, and verifies the recorded GitHub PR as merged ✅ pass live Live fork-sync product transcript
A diverged fork remains untouched while the local landing remains, and the command exits 3 with the reason and exact finishing command ✅ pass live Live fork-sync product transcript
A remote push rejection leaves the fork untouched and reports the remote error without undoing the local landing ✅ pass live Live fork-sync product transcript
A mismatched origin push URL is refused before either the configured fork or upstream remote moves ✅ pass live Live fork-sync product transcript
A checkout with no upstream remote lands locally, skips the push, and says that no fork is configured ✅ pass live Live fork-sync product transcript
A non-GitHub recorded PR gets a proved fork update and an explicit notice that PR state was not checked ✅ pass live Live fork-sync product transcript
The forge merge command refuses Firstmate's local-authoritative repository before calling gh or changing main or PR metadata ✅ pass live Live fork-sync product transcript
If the task record is replaced after the landing lock is released, fork sync still checks the PR captured from the locked original record ✅ pass live Live fork-sync product transcript
  • tests/fm-merge-local.test.sh
  • Read-only gh pr list -R BohnBawerick/firstmate --state merged --limit 3 --json number,url,mergedAt
  • Disposable live fixtures invoking bin/fm-merge-local.sh against real local bare Git remotes for successful push, divergence, remote rejection, mismatched push URL, no-upstream, non-GitHub PR, and task-record reuse cases
  • Normal authenticated gh api graphql read through bin/fm-merge-local.sh for merged PR https://github.com/BohnBawerick/firstmate/pull/32
  • Disposable live fixture invoking bin/fm-pr-merge.sh with a call-recording gh executable to prove refusal occurs before forge access
  • Verified fixture teardown, evidence creation, and worktree cleanliness with git status --short
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

An approved firstmate landing fast-forwarded only local main, so the fork
fell behind what the home runs, remote second mates synced stale code, and
landed PRs still showed open. After the local fast-forward, firstmate's own
repository now pushes local main to origin's main as a plain fast-forward
once fm-landing-remote verify passes, reads the branch back, and proves a
recorded PR merged with the shared forge read. A non-fast-forward, a failed
push, or an unproved read-back keeps the local landing, names why and the
command to finish, and exits 3. Project landings are unchanged.
@BohnBawerick
BohnBawerick merged commit ec6a79e into main Oct 4, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant