Skip to content

fix: harden merge queues and delivery validation - #15

Open
cisrd wants to merge 18 commits into
mainfrom
fm/fm-lot-livraison-pr-fiabilite
Open

cisrd wants to merge 18 commits into
mainfrom
fm/fm-lot-livraison-pr-fiabilite

Conversation

@cisrd

@cisrd cisrd commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Intent

Launch two additional grouped batches, with the objective of reducing the remaining task queue to zero where possible. For this delivery-reliability batch, finish the already identified Firstmate work as one coherent delivery: make the guarded merge path support merge-queue branches through enqueuePullRequest; never propose a retry the command parser will reject or that the caller already supplied; ensure Firstmate ships to cisrd/firstmate, where the captain has merge authority, rather than silently opening work against a read-only upstream; and finish the existing guard from #8 so a no-mistakes run that delivered nothing cannot be declared valid. Reuse and reconcile existing published work instead of duplicating it, and preserve every existing branch and pull request.

What Changed

  • Support guarded GitHub merge-queue enqueueing through enqueuePullRequest, with head-bound preflight checks, live queue confirmation, and parser-compatible retries that are not repeated when already supplied.
  • Reject read-only GitHub targets for autonomous delivery and document the supported delivery-target correction workflow.
  • Reject successful runs that skipped every mandatory delivery phase; report completed ledger entries without PR evidence as unverified.

Risk Assessment

⚠️ Medium: The guarded delivery changes are bounded and the selected fixes are implemented, but previously accepted limitations remain.

Testing

All four relevant suites passed with lsof available. Independent CLI replays confirmed real process reaping and simulated GitHub enqueue/retry behavior. Prior empty-delivery evidence remains available; no live PRs or branches were changed.

Evidence: Real leaked-process discovery and teardown

Source: Real leaked-process discovery and teardown

ok - a leaked descendant process rooted under the task's worktree is reaped by teardown, not left surviving

$ fm-teardown.sh task-x1 (isolated landed-work fixture; real lsof/process discovery)
/tmp/fm-teardown-tests.5s8CKc/leaked-process-reap/project: already current
teardown task-x1 complete (window firstmate:fm-task-x1, worktree /tmp/fm-teardown-tests.5s8CKc/leaked-process-reap/wt)
Backlog: task-x1 just finished (this home keeps no backlog at /tmp/fm-teardown-tests.5s8CKc/leaked-process-reap/data/backlog.md). Update /tmp/fm-teardown-tests.5s8CKc/leaked-process-reap/data/backlog.md - move task-x1 to Done, keep Done to the 10 most recent, then re-scan Queued and dispatch only work whose blockers are gone and date is due.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no live watcher process holds this home lock (last beat: 0s ago).
●  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 a missing or failed watcher cycle with the Pi tool fm_watch_arm_pi, or restart Pi with -e ~/.no-mistakes/worktrees/a03e7f5d4084/01M20Y4NWNXE2DM2P601DTGMY5/.pi/extensions/fm-primary-turnend-guard.ts -e ~/.no-mistakes/worktrees/a03e7f5d4084/01M20Y4NWNXE2DM2P601DTGMY5/.pi/extensions/fm-primary-pi-watch.ts if the extensions are not loaded.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
teardown: reaping leaked worktree process(es) for task-x1: 2649674
Evidence: Queue retry and GraphQL enqueue replay (simulated GitHub)

Source: Queue retry and GraphQL enqueue replay (simulated GitHub)

Behavioral CLI replay using simulated GitHub responses; no live PR mutation.
ok - fm-pr-merge's merge-queue refusal names a retry that base branch accepts

$ execute fm-pr-merge.sh emitted --queue retry
armed: state/task-x1.check.sh
verified: https://github.com/example/repo/pull/71 is queued (state=OPEN, merged=false, isInMergeQueue=true)
WARNING: watcher still down (same stale episode; last beat: never, grace 300s) - full banner already printed this episode.

Observed forge requests:
pr view https://github.com/example/repo/pull/71 --json headRefOid -q .headRefOid
api graphql -f query=query($owner:String!,$repo:String!,$number:Int!){repository(owner:$owner,name:$repo){pullRequest(number:$number){state merged isInMergeQueue baseRefName}}} -F owner=example -F repo=repo -F number=71 --jq .data.repository.pullRequest | "state=" + (.state // ""), "merged=" + (.merged | tostring), "queued=" + (.isInMergeQueue | tostring), "base=" + (.baseRefName // "")
api --paginate repos/example/repo/rules/branches/main --jq .[] | select(.type == "merge_queue") | "merge_method=" + (.parameters.merge_method // "")
pr view https://github.com/example/repo/pull/71 --json headRefOid -q .headRefOid
api graphql -f query=query($owner:String!,$repo:String!,$number:Int!){repository(owner:$owner,name:$repo){viewerPermission pullRequest(number:$number){id state merged isInMergeQueue baseRefName headRefOid commits(last:1){nodes{commit{statusCheckRollup{state}}}}}}} -F owner=example -F repo=repo -F number=71 --jq .data.repository as $r | $r.pullRequest | "state=" + (.state // ""), "merged=" + (.merged | tostring), "queued=" + (.isInMergeQueue | tostring), "base=" + (.baseRefName // ""), "node=" + (.id // ""), "head=" + (.headRefOid // ""), "checks=" + (.commits.nodes[0].commit.statusCheckRollup.state // "NONE"), "permission=" + ($r.viewerPermission // "")
api --paginate repos/example/repo/rules/branches/main --jq .[] | select(.type == "merge_queue") | "merge_method=" + (.parameters.merge_method // "")
api graphql -f query=mutation($pullRequestId:ID!,$expectedHeadOid:GitObjectID!){enqueuePullRequest(input:{pullRequestId:$pullRequestId,expectedHeadOid:$expectedHeadOid}){mergeQueueEntry{id state}}} -f pullRequestId=PR_NODE -f expectedHeadOid=8484848484848484848484848484848484848484 --jq .data.enqueuePullRequest.mergeQueueEntry | "entry_id=" + (.id // ""), "entry_state=" + (.state // "")
api graphql -f query=query($owner:String!,$repo:String!,$number:Int!){repository(owner:$owner,name:$repo){pullRequest(number:$number){state merged isInMergeQueue baseRefName}}} -F owner=example -F repo=repo -F number=71 --jq .data.repository.pullRequest | "state=" + (.state // ""), "merged=" + (.merged | tostring), "queued=" + (.isInMergeQueue | tostring), "base=" + (.baseRefName // "")
ok - fm-pr-merge refuses a read-only queue destination before enqueueing
Evidence: Prior empty-delivery CLI rejection

Source: Prior empty-delivery CLI rejection

Isolated real CLI with simulated no-mistakes completed empty-diff run:
$ fm-crew-state.sh feat-empty-diff
state: failed · source: run-step · not validated: run kept no change - review, test, document, lint, push, pr and ci were all skipped
- Outcome: 🔧 1 issue found → auto-fixed ✅ across 2 runs (23m43s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 4 issues found → auto-fixed ✅
  • 🚨 bin/fm-pr-merge.sh:635 - The head-continuity check reads metadata after record_pr_metadata has refreshed it. Concrete sequence: readiness records head A; another push replaces it with B; --queue calls fm-pr-check.sh, which overwrites pr_head with B (or removes it on lookup failure); preflight then accepts B and enqueues it. expectedHeadOid only protects pushes after preflight, not this replacement. Preserve the previously recorded head before registration and compare it before refreshing metadata. Extend the regression to change the head before invoking the wrapper, rather than only between its two live reads.
  • ⚠️ bin/fm-pr-merge.sh:156 - Simplification: --no-method, --method=queue, and --method queue introduce three alternative queue spellings absent from the base implementation. The stated intent requires a supported enqueue operation and runnable, non-repeated retry guidance, not compatibility with intermediate implementations in this branch. Recommend retaining only --queue and removing the additional acceptance paths and alias-specific tests.
  • ⚠️ tests/fm-teardown.test.sh:379 - Simplification: the newly added add_gh_pr_open_for_head helper has no callers and duplicates the open-state fixture already provided by add_gh_pr_state_for_head. No intent requirement needs this parallel implementation. Remove the unused helper.
  • ⚠️ tests/fm-crew-state.test.sh:484 - The full-delivery control fixture uses outcome: checks-passed, which bypasses the new vacuous-success guard and unconditionally reports done. Consequently, test_completed_run_with_full_delivery_still_reads_done would pass even if the guard incorrectly rejected an all-completed table. Remove this outcome line or use outcome: passed so the control exercises the changed classification path.

🔧 Fix: Remove intermediate queue aliases and unused teardown fixture
✅ Re-checked - no issues remain.

🔧 **Test** - 1 issue found → auto-fixed ✅
  • ⚠️ tests/fm-teardown.test.sh:3296 - Teardown testing stopped at test_leaked_worktree_process_is_reaped: the sleeper survived teardown. This host lacks lsof, needed to discover the fixture's detached process. Installing packages is prohibited in this phase; provide lsof and rerun this test to complete process-reaping verification.
  • bash tests/fm-pr-merge.test.sh passed.
  • bash tests/fm-pr-check-security.test.sh passed on standalone retry after the initial combined command timed out.
  • bash tests/fm-crew-state.test.sh passed.
  • bash tests/fm-teardown.test.sh passed queued-PR preservation checks, then failed detached-process reaping.
  • Executed temporary fixture drivers through the real merge and crew-state CLIs, replayed emitted retry arguments, and captured queue, permission-refusal, metadata, and empty-delivery outputs; removed both drivers.
  • command -v lsof confirmed the missing dependency; git remote -v confirmed cisrd/firstmate; final git status --short was clean.

🔧 Fix: Verify detached-process reaping with restored lsof dependency
✅ Re-checked - no issues remain.

  • command -v lsof confirmed the supplied executable.
  • bash tests/fm-teardown.test.sh ran the complete teardown suite, including leaked-process reaping.
  • bash tests/fm-pr-merge.test.sh exercised enqueue, runnable retry guidance, permissions, and merge verification.
  • bash tests/fm-crew-state.test.sh exercised empty-delivery rejection and delivery-state classification.
  • bash tests/fm-pr-check-security.test.sh exercised PR registration and poll security.
  • Replayed test_leaked_worktree_process_is_reaped independently and captured actual teardown output.
  • Replayed test_github_queue_retry_guidance_is_runnable and test_queue_preflight_refuses_read_only_repository using existing fixtures; captured CLI output and GraphQL requests.
  • Reviewed prior queue and empty-delivery CLI evidence; removed transient harnesses and confirmed a clean worktree.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Alex William added 18 commits September 8, 2026 17:45
When a branch's commits are already contained in the rebase base, the
pipeline loses the whole diff, logs "empty diff after rebase, skipping
remaining steps", and still records the run as completed. Review, test,
document, lint, push, pr and ci never execute and no PR is opened, yet
fm-crew-state.sh mapped that terminal success straight to done, so a run
that validated and delivered nothing read as shippable.

Read the steps table instead of the result word: when every mandatory
delivery phase is present and skipped, report the run failed and say why.
Positive evidence is required in both directions, matching the existing
held-green reclassification - an absent table, or any mandatory row that
is missing or not skipped, leaves the run's own reported result untouched,
so a real delivery and a partial skip both stay done.

Record at project-management the measured supported way to change a
delivery target: the PR target is the clone's own origin remote, so it
changes by repointing origin and re-running no-mistakes init, which also
refreshes the gate mirror. Editing the gate mirror's remote URL alone
changes neither the registration nor its tracking refs.
GitHub merge-queue rulesets refuse any explicit merge method, and the
unguarded --squash default made the guarded merge path unusable on those
branches. --method=queue and --no-method now skip that default without
forwarding a strategy, matching the GitLab "let the project decide" rule.

A queued pull request stays OPEN until it lands, so the merge poll and
teardown still wait for MERGED rather than treating enqueue as landing.
…ategy

Dropping only the queue token forwarded --squash, --merge, or --rebase,
and a merge-queue branch would reject that merge. Name both incompatible
requests and refuse before the forge is called.
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