Repository navigation
feat(bin): prove native GitHub stacks before firstmate calls anything a stack - #31
Merged
Merged
Conversation
… a stack PRs whose bases merely point at each other were twice presented as a stack when GitHub reported none, and stack instructions to workers named neither how to create a native stack nor how to verify one. - bin/fm-stack-check.sh proves PRs, given bottom to top, are one native GitHub stack: the same stack number on every PR, positions 1..N in order, size N, and each base the branch of the PR below (the bottom on the stack base; a PR above a merged layer may target the stack base). It reads the REST pull request stack field at API version 2026-03-10 and names every PR that is outside the stack or out of place. - bin/fm-brief.sh --stack (direct-PR only) scaffolds the stacked-chain variant: build or adopt the stack with gh stack (link for existing PRs, keeping their numbers; init/add/submit for new work), local checks in every PR body, no no-mistakes, and a done line quoting the passing checker proof. - bin/fm-pr-check.sh records a stack with every PR bottom to top, only after the checker proves it, on the top PR; a stacked-chain task cannot be recorded as a lone PR except to re-arm its already-recorded top PR, as relaunch does.
Firstmate never merges a stacked PR; stacks are used only in repositories the captain alone merges. The single-URL refusal for a stacked task no longer describes a top-PR merge path: it names the PR already recorded, says how to record the stack, and says the captain merges stacks. The stack brief now says the captain merges the stack.
SchokoShake
force-pushed
the
fm/fm-native-stack-mechanism
branch
from
September 17, 2026 14:47
abc4e26 to
275f75a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Give firstmate a mechanism, not just a note, for producing REAL native GitHub stacked pull requests and proving them.
The captain (2026-09-17): "firstmate should know how to properly produce stacks", after two failures the same day: PRs whose bases merely pointed at each other were presented as a stack (GitHub reported no stack), and stack instructions to workers did not name how to create or verify one.
Authoritative source: GitHub's docs (About stacked pull requests, quickstart, gh stack CLI reference). Key facts, verified against the doc and
gh stack --help: a stack is a server-side object;gh stack link <bottom> ... <top>turns existing PRs or branches into a stack and corrects bases;gh stack init/add/submitbuild a new one; REST exposes membership on a PR (gh api -H 'X-GitHub-Api-Version: 2026-03-10' repos/<o>/<r>/pulls/<n> --jq .stack-> number, position, size, base), which is how a stack is proven; CI runs for every PR in a stack; merging the bottom auto-rebases the rest.Deliverable, keep it small and owned once:
bin/fm-stack-check.sh <pr-url>...(bottom to top): exits 0 only when every PR reports the same non-null stack number, positions 1..N in the given order, size N, and each PR's base is the branch of the PR below it (the bottom's base is the stack base). On failure it names exactly which PR is not in the stack or out of place. Tests with a stubbedgh.bin/fm-brief.sh(--stack), used whenever dependent work ships as a chain: its definition of done requires creating or updating the native stack with gh stack (link for existing PRs, init/add/submit for new work), keeping PR numbers when adopting existing PRs, local checks per PR in the body, NOT running no-mistakes (trunk-only), and a done line that includes the passingfm-stack-check.shoutput. Chained bases alone never satisfy it.bin/fm-pr-check.shor the done-line handling), use the checker so firstmate cannot call something a stack that GitHub does not report as one. Keep this to the smallest hook that makes the check unavoidable.Constraints: load firstmate-coding-guidelines before editing; never run a test suite with a live FM_HOME exported; never stop, restart or update the shared no-mistakes daemon.
Decisions made while implementing (deliberate, not mistakes):
stack: {base: {ref, sha}, id, number, position, size}; merged 811/812 (stack fix(bin): recover Codex session locks without process inspection kunchenguid/firstmate#813) shows the upper PR retargeted to the stack base after the lower merged, per GitHub's documented retargeting. So the checker accepts the stack base as the base of a PR whose lower neighbour is merged (either that or the lower head); for unmerged layers the base must be exactly the lower PR's head. The zeigmal_cms 377/382 chain that was wrongly called a stack is correctly refused ("GitHub reports no stack").error:line per problem naming the PR, its given position, and what GitHub reports, then a summary; success prints exactly one linestack ok: #<n> base=<branch> size=<n>: <urls...>fit for a done line. Fields are joined with the ASCII unit separator, not tabs, because bash collapses whitespace IFS runs and an empty stack field would shift the rest.--stackrequires--mode direct-PR(refused with no-mistakes, local-only, scout, secondmate); its contract line isDelivery contract: mode=direct-PR stack=native, which fm-spawn's existing mode parse still reads as direct-PR (test added). Ordinary briefs stay byte-identical. Rule 1 forbids pushing outside the stack andgh stack merge.gh stack submit --autois named because the worker's shell is non-interactive.<task-id> <bottom-url> ... <top-url>; with more than one URL it runs fm-stack-check.sh before any side effect and records nothing unless proven; pr=, pr_head, pr_base, the merge poll, and the backlog link follow the TOP PR, because stacks merge bottom-up so the top merging means every layer landed. A task whose brief carries the stack contract line is refused with a single URL, EXCEPT when that URL is the pr= already recorded (the first record proved it) - this keepsbin/fm-control.sh relaunch, which re-arms with the recorded single pr=, and fm-pr-merge.sh on the recorded top PR working.What Changed
bin/fm-stack-check.sh <bottom-pr-url> ... <top-pr-url>reads each PR'sstackfield from GitHub's REST API (version2026-03-10) and exits 0 only when every PR reports the same stack number, positions 1..N in the given order, size N, and correctly chained bases. The stack base is also accepted as the base of a PR whose lower neighbour has merged. Success prints onestack ok: #<n> base=<branch> size=<n>: <urls>line. Each failure prints anerror:line naming the PR, its given position, and what GitHub reports. Usage errors (fewer than two URLs, a non-github.com URL, PRs in different repositories) exit 2 before any GitHub call.bin/fm-brief.shgains--stack, accepted only on ship briefs with--mode direct-PR, and writes the contract lineDelivery contract: mode=direct-PR stack=native. Its definition of done requires a native stack built withgh stack(init/add/submit --autofor new work,linkfor existing PRs, keeping their numbers) and local check results in each layer's PR body. It forbids/no-mistakesandgh stack merge, and requires a done line quoting the passingfm-stack-check.shoutput. It states that the captain merges the stack. Ordinary briefs are unchanged.bin/fm-pr-check.shnow accepts<task-id> <bottom-url> ... <top-url>. With more than one URL it runsfm-stack-check.shbefore any side effect and records nothing unless the stack is proven;pr=,pr_head=,pr_base=, the merge poll, and the backlog link then follow the top PR. A task whose brief carries the stack contract line is refused with a single URL, unless that URL is thepr=already recorded. The refusal names the recorded PR and says firstmate never merges a stacked PR.bin/fm-test-run.shmaps the new script and its test into thepr-forgefamily.AGENTS.mdsection 7,docs/architecture.md, anddocs/scripts.mdget short cross-references. Tests cover the checker with a stubbedgh, the--stackbrief, thefm-pr-check.shstack hook, andfm-spawn's mode parse of the stack contract line.Risk Assessment
✅ Low: The change is additive and fail-closed: a new read-only checker, an opt-in --stack brief variant that leaves ordinary briefs byte-identical, and a fm-pr-check.sh hook that validates every URL and proves the stack before any side effect. I confirmed gh's jq accepts the unit-separator field join and that the brief's gh stack commands match
gh stack --help. Every required intent item is present, and bin/fm-pr-merge.sh is untouched as required.Testing
I ran the four suites the change touches (stack-check, brief, pr-check-security, task-delivery) with no FM_HOME exported, and all passed. I then ran
bin/fm-stack-check.shlive and read-only against real GitHub stacks and against real non-stack chains. It proved 2-layer, 4-layer and merged stacks. It refused a chained-base pair with no stack, a wrong order, a missing layer, a PR from another stack and usage errors, with the documented exit codes and per-PR messages. In an isolated temp home I drove the real--stackbrief through the realfm-pr-check.shhook with a fixturegh. Nothing was recorded or armed until the stack was proven. After proof, the top PR was recorded and the merge poll armed. The relaunch re-arm worked, and a lone lower layer was refused with a message naming the recorded PR and stating that the captain merges stacks. I also confirmed that everygh stackflag the brief names exists in the installed extension, that ordinary briefs are unchanged from base, and thatfm-pr-merge.shis untouched. Evidence is two CLI transcripts and the demo script. This is a CLI-only change, so there are no screenshots. The live transcript has the private organisation, repository and branch names redacted, because the evidence branch is public.Evidence: Live read-only fm-stack-check.sh transcript against real GitHub stacks (private names redacted)
Source: Live read-only fm-stack-check.sh transcript against real GitHub stacks (private names redacted)
Evidence: End-to-end transcript: --stack brief, then fm-pr-check.sh hook refusing unproven stacks and recording a proven one
Source: End-to-end transcript: --stack brief, then fm-pr-check.sh hook refusing unproven stacks and recording a proven one
Evidence: Demo script that produced the end-to-end transcript (throwaway FM_HOME, fixture gh served through real jq)
Source: Demo script that produced the end-to-end transcript (throwaway FM_HOME, fixture gh served through real jq)
Evidence: Checker verdicts, live (excerpt)
Evidence: fm-pr-check.sh hook (excerpt)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-stack-check.sh:106- The per-PRgh apiread discards stderr (2>/dev/null), so an unauthenticated gh, a 404, a rate limit, and an unsupported API version all collapse into the same 'could not read the PR from GitHub' line. The check still fails closed (exit 1, nothing recorded), so this is only a diagnosability tradeoff: a worker or firstmate hitting an auth/API problem cannot tell it apart from a wrong PR number without re-running gh by hand. Optional follow-up: append gh's first stderr line to the problem message.bin/fm-stack-check.sh- The intent says the zeigmal_cms 377/382 chain is refused with "GitHub reports no stack". Live today GitHub's REST API reports both PRs as native stack fix(wake-lib): derive Linux pid identity from /proc ticks, not ps lstart kunchenguid/firstmate#392 (positions 1/2, size 2, base main), so the checker correctly printsstack okfor them now; the chain has evidently been linked since. The no-stack refusal was instead demonstrated live on another real chained pair in the same repo (370 -> 372,stack: null), which is refused with each PR named. Not a code defect.bash tests/fm-stack-check.test.sh(no FM_HOME exported) - all passbash tests/fm-brief.test.sh- all pass, including the --stack variant and the --stack refusal casesbash tests/fm-pr-check-security.test.sh- all pass, including 'records a stack only when GitHub proves it, and a stacked task never as a lone PR'bash tests/fm-task-delivery.test.sh- all pass, including fm-spawn reading thestack=nativecontract line as direct-PRLive read-onlybin/fm-stack-check.sh <bottom> ... <top>on real private stacks: an open 2-layer stack, an open 4-layer stack, and a merged stack whose upper PR was retargeted to the stack base all exit 0 with onestack ok:lineLive read-onlybin/fm-stack-check.shnegative cases: a chained-base pair withstack: null, reversed order, a 4-layer stack given with 3 URLs, and a PR from another stack all exit 1 with per-PRerror:linesbin/fm-stack-check.shusage errors: a single URL, cross-repository URLs and a GitLab URL all exit 2Livegh api -H 'X-GitHub-Api-Version: 2026-03-10' repos/<o>/<r>/pulls/<n> --jq '{stack,...}'confirms the RESTstackshape the checker reads: number, position, size and base.ref, or nullManual end-to-end in a throwaway FM_HOME (stack-hook-e2e-demo.sh), part 1: realbin/fm-brief.sh task-a some-proj --mode direct-PR --stackbrief, and--stackrefused with no-mistakes, local-only and scoutManual end-to-end, part 2, with a fixture-backedgh(real jq): realbin/fm-pr-check.shagainst that brief refuses chained-only PRs and a lone PR and records nothingManual end-to-end, part 3: a proven stack is recorded on the top PR with the merge poll armed; re-arming with the recorded top PR alone is allowed; a lower layer alone is refusedgh stack init|add|submit|link --help(gh-stack v0.1.0) confirms every command and flag the brief names exists:--base,--auto,--open, and link by PR number or URLScaffolded ordinary no-mistakes, direct-PR and local-only briefs from base 938dff7 and from the target: they differ only by the temp-home pathgit diff --quiet 938dff7 affd1db -- bin/fm-pr-merge.shconfirmsbin/fm-pr-merge.shis untouched✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.