fix(bin): claim a merged PR only when the pr step and every step after it completed - #3527
Open
loom-loki wants to merge 16 commits into
Open
fix(bin): claim a merged PR only when the pr step and every step after it completed#3527loom-loki wants to merge 16 commits into
loom-loki wants to merge 16 commits into
Conversation
…PR path A Forgejo pull request URL carries the plural /pulls/ segment, which fm_pr_url_parse did not accept, so bin/fm-pr-check.sh and bin/fm-pr-merge.sh both refused one outright. Nothing could record pr= or pr_head=, no merge poll could be armed, and merging had to happen outside the guard entirely. Add forgejo as a third provider across the PR path: - fm_pr_url_parse gets its own host, owner, and repository rules rather than a loosened GitHub or GitLab rule. The shared lowercase-DNS host shape moves into fm_pr_dns_host_valid so it is stated once; GitLab's accepted set is unchanged. github.com and gitlab.com are refused as Forgejo hosts for the same reason github.com is already refused as a GitLab one. - bin/fm-pr-poll.sh reads forgejo-axi pr merged and wakes only on an exact merged proof. The poll source stays byte-for-byte identical for every task and the identity stays in the private sidecar. - bin/fm-pr-merge.sh verifies one live mergeability read, reports every failing condition plus the forge's own reasons, and binds the merge to the verified head with forgejo-axi pr merge --expected-head. Both the poll and the merge pass the host from the validated record as --base-url. forgejo-axi reads only owner/repository/number out of a pull request URL and still sends the request to whatever host its own configuration resolves, so passing the URL alone would let an ambient default answer for the host the record names. An absent forgejo-axi refuses at arming and at merge rather than watching nothing, matching how the GitLab path handles an absent glab. docs/forgejo-merge-watch.md records the live evidence against a public Forgejo instance.
…unmerged Forgejo proof
An upstream review flagged that a Forgejo URL naming "-" as its repository
parsed and armed a merge poll. Forgejo reserves that name outright, so the
poll watched something the forge cannot host and could never wake - the
silent failure this validation exists to prevent.
Forgejo's own rules, checked against its source rather than inferred:
reservedRepoNames = []string{".", "..", "-"}
reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"}
"." and ".." were already refused; "-" was not. IsUsableName also lowercases
before comparing, so the reserved suffixes are case-insensitive on the forge
while the patterns here were not: "repo.GIT" and "repo.Wiki" were accepted.
Both gaps are fixed, in bin/fm-pr-lib.sh and in bin/fm-pr-poll.sh, which
re-validates rather than trusting its sidecar. Each side now points at the
other so a later change cannot fix one and miss it.
Two things deliberately left alone, with the reasoning in the comments:
The same review claimed consecutive dots are invalid. They are not.
AlphaDashDotPattern is [^\w-\.] and carries no consecutive-character rule for
repositories; that restriction is invalidUsernamePattern's, and it already
applies to the owner segment here. Refusing "a..b" would make this stricter
than the forge and reject a repository it can genuinely host, which is a worse
defect than the one reported. A test now pins that, so the claim cannot be
adopted later by accident.
The forge's reserved USERNAME list stays unencoded for the owner. It is
version-varying policy that would rot into refusing owners the forge accepts,
unlike "-", which can never name a hostable repository.
feat(bin): watch and merge Forgejo pull requests through the guarded PR path
# Conflicts: # docs/gitlab-merge-watch.md
…r it completed (#2) * fix(crew-state): stop reporting a skipped PR step as a merged PR A no-mistakes run reaches outcome=passed once its steps finish without failing, which includes a run whose pr and ci steps were skipped because the pipeline could not resolve the push provider. Such a run opened and merged nothing, yet fm-crew-state.sh mapped outcome=passed to the fixed detail "run passed: PR merged/closed", asserting a merge it had never observed. Confirmed on no-mistakes v1.60.2: run 01M1EA5NJVP18AE7SPY5MBYW42 reported `pr,skipped,0,17` and `ci,skipped,0,16` under outcome=passed while the forge still had that branch's pull request open and unmerged. The passed mapping now reads the run's own steps table and claims a merge only when the pr step actually completed; a skipped or absent pr step reports what happened and states that merge state is unknown to the run. fm-crew-state still reports only the run's state, so the forge remains the sole authority on merge state. The generic step-row reader added for this also now backs nm_ci_step_status, which keeps its deliberate narrowing to the running and fixing values its caller acts on. Every other outcome, gate, awaiting and run-step branch is unchanged. * fix(crew-state): read every step row and prove the findings-row guard Two hardenings to the step-row reader introduced with the skipped-PR fix. The status word is now matched loosely rather than against a narrow lowercase class. The numeric third column is what separates a step row from a findings row, so the status itself does not need constraining, and constraining it meant an unrecognized or newly added status word would read as no row at all. Checked against all 495 step rows that `no-mistakes axi status --run` renders for every run in the local v1.60.2 store: all 495 are read, and every row the narrower ci-only predicate this replaced used to match is still matched. The numeric-column guard now has its own negative case. The findings table is emitted before the steps table, so a finding whose id is `pr` is matched first without the guard and shadows the real pr step row, reporting the finding's severity as the step status. The new test pins that collision and was demonstrated failing with the guard removed: "run passed, PR step warning" instead of "PR step skipped". * no-mistakes(document): correct stale outcome=passed merge claim in crew-state comment * no-mistakes(document): scope crew-state step-reader parity claim to what was verified * no-mistakes(review): require every step after pr to complete before claiming merge * no-mistakes(review): correct stale landed-case comment to match merge guard * no-mistakes(document): drop duplicated outcome=passed merge claim from ci-checks comment --------- Co-authored-by: ypadhi <yashrajpadhi@gmail.com>
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains, and the previously reported numeric findings-shadowing issue is resolved by structurally restricting parsed rows to the steps table. Reviews (2): Last reviewed commit: "no-mistakes(document): correct step-row ..." | Re-trigger Greptile |
…-row evidence comments
…mn shape
The step-row reader separated a step row from a findings row by requiring an
unquoted numeric third column: a step row's `duration_ms` is a number, while a
findings row's third column is a file path. That held only because the encoder
observed on v1.60.2 quotes a numeric-looking string field, so a finding on file
`123` rendered as `"123"` and fell out of the predicate. Nothing in the format
makes that quoting a guarantee, and the findings table is emitted BEFORE the
steps table, so a finding whose id is `pr` and whose file column rendered
unquoted would be matched first and shadow the genuine pr step row - reporting
the finding's SEVERITY as the PR step status, which is exactly the step the
merge claim depends on.
Scope the reader to the steps table by position instead: rows after the
`steps[N]{...}:` header and indented deeper than it, up to the first line that
dedents back to the header's level. The header match is anchored so
`active_steps[...]` cannot open the section. Every other table is then excluded
structurally, whatever its rows contain, and a change in how the encoder quotes
any column can no longer turn a finding into a step. Dropping the column-content
predicate also removes the opposite failure: a quoted `duration_ms` would have
made genuine step rows unreadable.
This is the same generalisation the surrounding code already applies twice -
state the rule so it holds by construction rather than special-casing the shape
that happens to be observable today.
The existing negative test is extended rather than replaced: its fixture's file
column is now parameterized, and the case runs for both an ordinary `a.go` and a
bare numeric `123`. The numeric case fails against the previous guard, reporting
`run passed, PR step warning: merge state unknown to the run` from the finding
instead of reading the real skipped pr step.
…duration comments
loom-loki
force-pushed
the
fm/fm-upstream-crew-state-merge-claim
branch
from
September 4, 2026 11:35
a2d883e to
2ce97fa
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
Contribute an already-written, already-proven fix upstream to this project. Nothing to design, nothing new to implement.
THE BUG: bin/fm-crew-state.sh claimed a no-mistakes run had merged a pull request when it had not. A run reaches outcome=passed once its steps finish without failing, which INCLUDES a run whose pr and ci steps were SKIPPED. Such a run opened and merged nothing, yet the reader mapped outcome=passed to the fixed detail "run passed: PR merged/closed", asserting a merge it had never observed. Verified on no-mistakes v1.60.2: run 01M1EA5NJVP18AE7SPY5MBYW42 reported pr,skipped and ci,skipped under outcome=passed while the forge still had that branch PR open and unmerged, and the work was reported as landed.
THE FIX (deliberate design decision): state the rule GENERALLY rather than special-casing one step. The merge claim is made only when the run own pr step AND every step it reports after that one actually completed. This is deliberate: the same false-merge claim existed one trigger over, when ci rather than pr was skipped, and both are closed by the one general rule. Do NOT reintroduce a per-step special case, and do NOT hardcode any step name after pr - the steps a merge claim depends on are read from the run own step order, because axi status carries no merge signal to key on directly. A run that passed with the pull request opened but checks unverified now reports that honestly instead of claiming a merge.
Implementation: generalises the previous ci-only nm_ci_step_status into nm_step_rows/nm_step_status and adds nm_passed_pr_detail. Reading the RIGHT row is load-bearing: the findings table is emitted BEFORE the steps table, so a finding whose id is
prcan be matched first and shadow the genuine pr step row, reporting the finding SEVERITY as the step status. nm_step_rows therefore reads only rows sitting INSIDE the steps table - after thesteps[N]{...}:header and indented deeper than it, up to the first dedent - with the header anchored soactive_steps[...]cannot open the section. nm_ci_step_status is deliberately kept as a narrow wrapper that returns only running/fixing so its caller falls through unchanged.CHANGED AFTER THIS PULL REQUEST WAS OPENED - and thanks to the automated reviewer for the catch, it was a good one: the first version of nm_step_rows told a step row from a findings row by requiring an unquoted NUMERIC third column, since a step row duration_ms is a number while a findings row third column is a file path. The review pointed out that a finding on a numeric file path such as
123would satisfy that predicate and shadow the genuine pr step. That shape does not occur on v1.60.2 - the encoder quotes a numeric-looking string field, so such a finding renders as "123" and falls out of the predicate - but that is an observation about one version quoting behaviour, not a guarantee the format offers, and the step a merge claim depends on is exactly the step a finding can be named after. The predicate is therefore now scoped by POSITION in the output instead of by column content, so a findings row cannot satisfy it however the encoder chooses to quote. This also removes the opposite failure the old predicate carried: had duration_ms ever rendered quoted, it would have made genuine step rows unreadable.ACCEPTED REFINEMENTS ALREADY APPLIED IN THIS BRANCH (do not undo, do not re-litigate):
<step>,running,0,0in the steps table beside<step>,running,<duration>,...in a separate active_steps table whose third column is a duration. The fixture and the comment both record that this shape was observed on that version rather than inferred, because a vendor output shape is a fact about a version, not a law. Mutation-tested: break the reader so it stops matching the active run steps row and that test fails with the exact incident signature, a green PR reading as still validating.TESTS: tests/fm-crew-state.test.sh covers the skipped pr step, a completed pr step with a skipped ci step, pr as the last reported step, a run with no pr row at all, the still-valid genuinely-merged case, the active-run both-tables case above, and a NEGATIVE case for the findings-row guard. That negative case is deliberate and has been verified to fail when the guard is removed - an earlier attempt at it would have passed with the guard gone, which is exactly what it must not do. That negative case now runs for two findings-row shapes - an ordinary file column (
a.go) and a bare numeric one (123) - and the numeric shape was verified to FAIL against the earlier column-content predicate, reporting the finding severity as the pr step status, and to pass once the reader was scoped to the steps table.SCOPE CONSTRAINT (explicit, deliberate): exactly two files - bin/fm-crew-state.sh and tests/fm-crew-state.test.sh. Touching a third file is out of scope. This fix carries no dependency on any forge-specific or provider-specific support code and must not acquire one.
DELIVERY: this is a contribution to somebody else project. The pull request body must stand on its own for a maintainer with none of this context: what the bug is, how it reproduces, why the fix is stated generally rather than per-step, and what the tests prove. Do not merge. Stop when the pull request is open and its checks are green.
NOTE ON THIS RUN: a previous run of this same branch reached the push step and failed there on a transient SSH authentication error from the forge; the identical command has since succeeded repeatedly. The two review-fix commits from that run are preserved on this branch and custody was returned through the supported recovery path. Nothing else changed.
What Changed
bin/fm-crew-state.shmapped a terminaloutcome=passedrun to the fixed detailrun passed: PR merged/closed, asserting a merge it had never observed. A run reachesoutcome=passedonce its steps finish without failing, which includes a run whoseprandcisteps were skipped - the shape no-mistakes takes when it cannot resolve a push provider, so nothing was opened or merged. Reproduced on no-mistakes v1.60.2: run01M1EA5NJVP18AE7SPY5MBYW42reportedpr,skipped,0,17andci,skipped,0,16underoutcome=passedwhile the forge still had that branch's pull request open and unmerged, and the work was reported as landed.nm_passed_pr_detailclaims a merge only when the run's ownprstep and every step the run reports after it readcompleted; otherwise the detail names the step that stopped short and ends inmerge state unknown to the run. The rule is stated generally rather than special-cased on one step because the same false claim existed one trigger over (axi run --skip=cileavesprcompleted,ciskipped, PR open), and no step name afterpris hardcoded - the steps a merge claim depends on are read from the run's own step order, sinceaxi statuscarries no merge signal to key on directly. Supporting change: the ci-onlynm_ci_step_statusis generalised intonm_step_rows/nm_step_status, needed because the findings table is emitted before the steps table and a finding whose id isprwould otherwise shadow the real step row and report the finding's severity as the step status.nm_step_rowsselects rows by their position in the output - those inside thesteps[N]{...}:table, with the header anchored soactive_steps[...]cannot open the section - rather than by matching a numeric third column, so no findings row can satisfy it regardless of how the encoder quotes a numeric-looking field.nm_ci_step_statusremains as a narrow wrapper returning onlyrunning/fixingso its caller falls through unchanged.tests/fm-crew-state.test.shcovering a skippedprstep, a completedprstep with a skippedcistep,pras the last reported step, a run with noprrow at all, the still-valid genuinely-merged run (fixture from real run01M1669Y82JTHWEBSG7PR2TKNH), an active run rendering both thestepsandactive_stepstables (fixture built fromaxi statuscaptured mid-step on live v1.60.2 runs, pinning that the numeric guard did not break the green-checks override behind the PR fix(backends): scope zellij tabs by firstmate home #252 incident), and a negative case that fails if the findings-row guard is removed, now run for both an ordinarya.gofile column and a bare numeric123- the numeric shape fails against the earlier column-content predicate and passes once the reader is scoped to the steps table. Every terminal-detail assertion was also tightened from a substring fragment to the complete expected string.Risk Assessment
✅ Low: A well-bounded two-file change to one read-only reporting helper that replaces an over-claiming fixed string with a conservative, generally-stated derivation, backed by fixtures copied from real observed output and a negative test for the load-bearing guard; every traced input produces a correct detail and no downstream consumer parses the changed string.
Testing
I reproduced the reported bug end-to-end before testing the fix: running bin/fm-crew-state.sh from the base commit against the real v1.60.2
axi statusoutput for run 01M1EA5NJVP18AE7SPY5MBYW42 (pr and ci skipped, outcome passed) printsrun passed: PR merged/closed, and this branch printsrun passed, PR step skipped: no PR was opened or merged by the run, merge state unknown to the run; the same before/after holds for the--skip=cishape, while the genuinely landed run still reads as merged on both. I followed the detail through to the surfaces a captain and firstmate actually see - the /bearings Underway row and the fleet-snapshot JSON - and measured the longest new detail against the board's 90-char truncation: it is 98 characters and IS clipped there, rendering as "...merge state unknown to" with the trailing "the run" cut. The "by the run" scoping and the "merge state unknown" sense both survive the cut, and the fleet-snapshot JSON firstmate consumes carries the full untruncated string, so no consumer sees a claim stronger than the run can support. An earlier revision of this section said 87 characters and survived intact; that stopped being true when the "by the run" scoping was restored, and this is the corrected measurement. Each of the five new merge-claim tests fails individually against the pre-fix reader with the exact false-merge signature and passes here, and the two invariant tests that necessarily pass both before and after are pinned by mutation instead, re-run against the implementation actually in this diff: removing the steps-table scoping makes the findings-row test fail with a finding severity reported as the pr step status, and breaking the section boundary so an active run's own steps rows stop being emitted makes the active-run test fail with a green PR reading as still validating. The original numeric-column mutation has no analogue any more, because the predicate no longer inspects column content at all. The targeted suite and the downstream fleet-snapshot view suite are green, no other suite asserts on this detail string, and every mutation below was applied to a throwaway copy of this branch's head and reverted, leaving the branch itself untouched. This is a CLI text-line change with no rendered UI surface, so the evidence is CLI transcripts rather than screenshots.Evidence: Before/after reader transcript over the real observed axi status shapes
Evidence: Corrected detail on the captain-facing /bearings row and the fleet-snapshot JSON
Evidence: Regression tests failing pre-fix, plus the mutations re-run against this diff's implementation
Evidence: The bug and the fix, in one pair of lines
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-crew-state.sh:329- The "Verified against all 495 step rows ... all 495 are read" sentence is pre-existing context (it survives unchanged from 542edd9) and was written to evidence the REMOVED column-content predicate: it measured that 495 rows matched an unquoted-numeric-third-column regex. This commit replaces that reader with a positional one, so the equivalent property is now "all 495 rows sit inside a steps[N] section indented deeper than its header" - which the old scan did not test. The sentence now sits inside the paragraph describing the positional rule, so a reader (including the upstream maintainer this is contributed to) takes it as verification of the current reader. Neither the diff nor 0630c0d's commit message states the scan was re-run. This matters more than usual here because the change's own stated discipline (and the intent's) is that comments justifying the old rule must be rewritten rather than left to rot into false claims - this is the one evidence claim carried across the implementation swap. Remedy: either re-run the store-wide scan against the positional reader and say so, or scope the sentence to what it actually established. Flagging rather than fixing because only the author knows whether the scan was re-run.tests/fm-crew-state.test.sh:580- The comment says "The ci status must come from the steps rowci,running,0,0, never from the active_steps rowci,running,1m40s,...", but the fixture cannot fail on the "never" half. nm_step_status doesnm_step_rows | grep -E "^[[:space:]]*ci," | head -1, and in run_ci_monitoring_with_active_steps the steps row precedes the active_steps row, so head -1 selects the steps row even if active_steps rows leaked into the stream - and both rows would yield the same status wordrunninganyway. I confirmed this by tracing an intentionally unanchored header regex through the parser: the active_steps rows get appended after the steps rows and the derived ci status is unchanged, so the test still passes. The test IS a valid positive guard (dropping the steps rows makes CI_STEP_STATUS empty, the green-checks override never fires, andstate: donefails), which is what the comment's later "pins that the narrowing left the ACTIVE-run path intact" clause claims. Only the "never from the active_steps row" clause is unpinned. Given the author's stated standard that "an assertion that would pass either way is worthless here", narrow the comment to the positive property it actually pins.🔧 Fix: correct step-row evidence and active_steps test comments
2 issues (1 warning, 1 info) still open:
bin/fm-crew-state.sh:415- The skipped-pr detail emitsrun passed, PR step skipped: no PR was opened or merged, merge state unknown to the run. Commit d1d81a8 droppedby the runfrom the first clause while making the suffix uniform across the four branches. The sentence now asserts unscoped that no PR was opened or merged, and then says merge state is unknown to the run - self-contradictory, and false in the exact scenario this change exists for: the evidence comment at lines 390-394 records run 01M1EA5NJVP18AE7SPY5MBYW42 reportingpr,skipped,0,17under outcome=passed while the forge still had that branch's pull request OPEN and unmerged. Concrete path: a run whose push provider cannot be resolved skips pr and ci, reaches outcome=passed, and fm-crew-state rendersdetail: run passed, PR step skipped: no PR was opened or merged, ...for a branch whose PR is open on the forge. The file's own doc comment at line 35 keeps the correct scoping ("a skipped pr step means the run opened and merged nothing"); only the user-visible string lost it. The base text at 542edd9 (no PR was opened or merged by the run, merge state unknown to it) was accurate. Remedy is restoring the run scoping in that clause (e.g.no PR was opened or merged by the run) while keeping the uniformmerge state unknown to the runsuffix the tests now assert - but the suffix uniforming was a deliberate prior-round decision, so the author should confirm the wording rather than have it silently reverted.bin/fm-crew-state.sh:329- "the whole local v1.60.2 store ... all 73 runs" can be read as "73 runs produced by v1.60.2". In the store as it stands, 51 of the 73 runs were produced by v1.57.0 and only 22 by v1.60.2. This does not weaken the evidence - I reproduced the scan and the reader reads 657 of 657 rows with zero per-run mismatches against step_results - because output shape is set by the RENDERING binary (v1.60.2 for all 73), not by the producing version. If anything the claim is broader than stated: the reader also handles rows written by an older version. Noting it so the figure is not misread; no change required.🔧 Fix: restore run scoping in skipped-PR detail, pin full strings
1 info still open:
tests/fm-crew-state.test.sh:410- The fixture comment asserts and denies the same fact in one sentence. It says "the durations observed across those samples span both second scale (7s, 18s, 19s) and minute scale (2m34s, 3m41s ...), so the1m40sbelow is a duration string that version actually renders rather than a plausible-looking one invented to fill the column", then immediately concedes "only the step name and the durations differ here".1m40sis not in the cited observed set {7s, 18s, 19s, 2m34s, 3m41s}; what the evidence supports is the minute-scale SHAPE, not that literal string. The bin/fm-crew-state.sh counterpart added by the same commit (1cc57ee) got this right - "the minute-scale third column the test fixture carries is a shape this version actually renders" - but commit 0630c0d rewrote that block and dropped the sentence, leaving only the imprecise string-level claim in the test file. Remedy is wording only: claim the shape ("a minute-scale duration of the form this version renders") and keep the observed sample list, so the sentence stops contradicting its own next clause. No behavior is affected - theactive_stepsthird column is now rejected structurally and is never read.bin/fm-test-run.sh --changed --exclude-family real-herdr-gatedbin/fm-crew-state.sh:333- Out-of-scope consolidation worth a follow-up, reported rather than done. The nm_step_rows comment is now ~50 lines, over half of it a verification narrative (657 of 657 rows across 73 runs, ground-truthed against step_results; live mid-step captures on v1.60.2 eb4e379 built 2026-08-29). Under the placement tree in .agents/skills/firstmate-coding-guidelines/SKILL.md that is tier-5 material - 'active reusable verification for a current guarantee' - whose classified home is a maintainer-verification record under docs/verification/, which docs/documentation-audiences.md says is the surface allowed to carry dates, versions, exact commands and exact output. Tier 7 gives the script header the mechanics (the positional rule itself), not the evidence run. I did not move it, for three reasons: the file already carried comparable inline evidence before this change (the v1.32.2 / '360+ real run logs' paragraph at line ~466), so this change follows the file's established convention rather than introducing drift; the evidence is tightly coupled to why the parser is shaped this way, which is a legitimate reason to keep a distilled form inline; and extracting it would touch a third file, which this run's stated constraints make a stop-and-report condition. If the split is wanted, the shape would be a docs/verification/ record owning the scan and the live captures, with a one-line pointer left in the comment - not a synchronized second copy.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.