Skip to content

fix(update): synchronize GitHub forks before checking origin - #3

Merged
rub-a-dub-dub merged 8 commits into
mainfrom
fm/firstmate-teach-the-updater-to-sync-a-fo-15
Sep 19, 2026
Merged

rub-a-dub-dub merged 8 commits into
mainfrom
fm/firstmate-teach-the-updater-to-sync-a-fo-15

Conversation

@rub-a-dub-dub

Copy link
Copy Markdown
Owner

Defect fixed

/updatefirstmate fetched updates from origin, which on this installation is a GitHub fork, while the authoritative changes land on the fork's upstream. If the fork had not been synchronized with its upstream, the updater could fetch a stale origin and report already current even though the checkout was genuinely behind.

This PR makes fork synchronization mechanical and load-bearing:

  • Before comparing against origin, bin/fm-ff-lib.sh (ff_sync_origin_fork) discovers whether origin is a GitHub fork via the GitHub API and, if so, fetches the upstream's matching default branch and fast-forwards the fork to it with an ordinary non-forced push - no local branch, merge commit, or commit to the fork's default branch is created by this repository. An authoritative (non-fork) origin takes none of this machinery: the no-fork case is a byte-identical no-op to the prior behavior.
  • The relationship and branch names are discovered from GitHub, not hardcoded; the parent's fetch URL is derived from origin's own transport, so no separately named upstream remote is required.
  • Failure classification: an ordinary transport failure (offline, VPN, an unreachable remote host) is a reported skip and does not fail the whole run. A fork that is established but genuinely could not be synchronized (diverged, mismatched default branches, rejected push) fails loudly with a nonzero exit - this is exactly the case that used to silently degrade to "already current."
  • Currency verification survives failure to discover it: when gh-axi/node cannot answer at all (expired token, tool missing), fork-ness is treated as unknown rather than absent - the ordinary Git update still runs (so a non-fork install with a broken token isn't bricked), but the status line reads cannot confirm current: fork sync unavailable (<reason>) instead of already current, and a run-level origin-verified: yes|no line makes that verdict explicit and impossible to miss. That verdict is carried on the remote-secondmate route's result line (not to stderr, which the parent discarded before this change), so a local and a remote host can never disagree about whether a run was authoritative.

See .agents/skills/updatefirstmate/SKILL.md for the updated operator contract and bin/fm-update.sh / bin/fm-ff-lib.sh for the mechanics.

Review rigor

This went through five review rounds and sixteen findings before landing (no-mistakes axi review, ask-user gates escalated to the captain each time). The recurring theme across those sixteen findings was unrequested acceptance paths - extra "helpful" spellings or fallbacks nobody asked for - and it came up four separate times: a second URL-spelling acceptance arm, an insteadOf-alias fallback for origin identity, a www.github.com host alias, and (folded into the round-3 centralization) an overly broad exit-failure classifier. Every one of the four was removed rather than kept. Less surface, not more, was the right call each time.

What was deliberately NOT fixed, and why

  • Remote-secondmate route can still exit 0 on a genuine fork-sync failure. The local/main route's failure classifier (above) correctly distinguishes an ordinary network failure from an established fork that genuinely failed to sync - but that distinction does not yet survive the SSH boundary to a remote secondmate host: every way the remote command can fail (unreachable host, timeout, or a real fork-sync failure) currently collapses to the same generic path, which reports the run unverified but does not fail the parent's exit code. This installation currently has no registered secondmates (data/secondmates.md does not exist), so this gap affects nothing today. It is filed rather than fixed here: Remote secondmate route: a genuine fork-sync failure does not fail the whole /updatefirstmate run kunchenguid/firstmate#4875 (title: "Remote secondmate route: a genuine fork-sync failure does not fail the whole /updatefirstmate run"), with a pickup trigger of "the first time a secondmate is created on this installation."
  • A failed fork sync is not memoized per object store. fetch_once's per-object-store memo is only written on the fully successful path, so a store whose fork sync fails is re-discovered (re-fetched, re-pushed) by every later worktree sharing that store in the same run. Flagged in round 5 as info/no-op by the reviewer; observed and deliberately not acted on here.

Evidence: gh-axi contract verification

The fork-sync mechanism's only dependency on gh-axi's output shape was verified live against this installation's real fork, not assumed:

gh-axi api "/repos/rub-a-dub-dub/firstmate" --hostname github.com --full --jq "{fork: .fork, parent: .parent.full_name, default_branch: .default_branch}"
default_branch: main
fork: true
parent: kunchenguid/firstmate

--full preserves complete field values and --jq yields the flat key: value record the decoder expects, in exactly that shape. tests/fm-update.test.sh also pins the expected record shape explicitly so a future gh-axi flag or output-shape change fails a test instead of silently degrading through the soft discovery-failure fallback.

CI

Expect no CI. GitHub Actions has never run on this fork (rub-a-dub-dub/firstmate), so no checks will appear on this PR.

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