Skip to content

F: merge the task's published PR, not github.pullRequest (#121) - #122

Closed
mchwang wants to merge 2 commits into
feat/103-publish-prfrom
feat/121-merge-task-pr
Closed

mchwang wants to merge 2 commits into
feat/103-publish-prfrom
feat/121-merge-task-pr

Conversation

@mchwang

@mchwang mchwang commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Closes #121. Stacked on #110 (feat/103-publish-pr).

Problem

Since #103 the runner publishes its own pull request for each task. The guarded merge gate still inspected, merged and polled only github.pullRequest. A runner task's merge was therefore always blocked: the configured PR's head is not the task head, and the task's own PR counted as "another PR" in the already-fixed check. It could also target the wrong PR. Merge attempts did not record their PR, and nothing read the draft flag.

Change

  • Target (runner/merge.ts, web/server.ts):
    • With a runner block, the gate targets the task's published PR: the opened record in the configured repository and github.baseBranch whose opening began last.
    • GitHub must show that PR from the task branch (not a fork), into that base, with the record's marker as its first line, and as the only open PR from the branch. Anything else is a pull-request blocker.
    • github.pullRequest is optional with a runner block. If it is set and names another PR, merging is blocked.
    • Without a runner block, github.pullRequest is required (the server refuses to start without it) and is used as before.
    • A draft PR is blocked (draft) in both modes.
  • Pinning:
    • Every validation pass, and the third pass in queue mode, must resolve the same PR from the same opening.
    • Store.beginMergeAttempt re-reads the record in the transaction that saves the attempt. It must still be the opening that began last in its base, with the same number, and no opening or update may be in flight.
    • The attempt stores pullRequest in its JSON data, so no migration is needed. Reconciliation, the queue watermark, queue polls and gh pr merge use the attempt's PR and never resolve it again.
    • Attempts saved before F: merge the task's published PR, not github.pullRequest #121 use github.pullRequest.
  • Gateway (github/merge.ts):
    • Each call takes a target, and the status cache and in-flight read are keyed by it.
    • pr view now also reads and validates number and isDraft. For a published target it also reads body and isCrossRepository, and lists the open PRs from the task branch (one page; a full page fails closed).
    • If a read fails, the parallel reads are stopped and awaited before the inspection rejects.
  • Already-fixed check: the PR being merged plus every PR the task records in the repository count as its own.
  • Refactors: openingMarker moved to github/pull-requests.ts, and baseBranch moved to github/validate.ts (re-exported from runner/production.ts). The server can now check both without loading the runner.
  • Docs: guarded-merge.md (new "Which pull request (F: merge the task's published PR, not github.pullRequest #121)" section), merge-queue.md, pull-request-opening.md, README.

Evidence

Validated head: cca2dfd.

  • npm run typecheck: clean.
  • The 12 test files that touch the change: 513 passed. These are merge-task-pr (45, new), merge, runner-shutdown, review, publish, runner-production, cli, runner-publishing, store, gh-env, already-fixed and runner-start.
  • The full local suite could not finish: the host was overloaded (load average 160 to 230 from another VM), so tests timed out in files this change does not touch. Each of those files passed when rerun on its own. CI on this head is the full-suite record.
  • Guard proofs: each of these guards was removed and its test failed, then the code was restored:
    • the settlement PR check;
    • the second-pass and third-pass target checks;
    • the store guard against a newer opened record;
    • the store guard against an update in flight;
    • stopping the other reads when one fails;
    • the draft blocker;
    • the configured-PR blocker.

Review findings (independent review agent, round 1)

# Finding Outcome Rule
1 The branch-list and rule reads ran in Promise.all. A failing list read left the rule-read gh processes running past the inspection. Fixed: one read's failure stops the others, all are awaited, and a test holds a rule read. Existing rule: "A deadline must abort and await the underlying operation…" and the subprocess-settlement rules
2 The server test was close to vitest's 5 s timeout. Fixed: a 60 s timeout on that describe block. One-off: host load
3 The replay test passed even if the refusal was never saved. Fixed: the resend goes to a coordinator that would now merge. Existing rule: "A test MUST fail when the feature it tests is broken"
4 A publish could start after admission. No change: already refused by #assertPublishable (needs running or needs human; refuses during an active merge). Covered by an existing guard
5 An older opening adopted later is not "latest". Docs and comments now say "the opening that began last". In that case the branch list blocks, which fails closed. One-off: wording
6 The docs claimed "no recorded PRs without a runner block". Fixed the wording. Existing rule: verify behavioural claims
7–10 Gaps in tests: the settlement interleaving, per-target in-flight sharing, store guard branches, the config-mode draft blocker, the third pass, the watermark PR, a legacy poll leaving the attempt unchanged, malformed list entries, and a recorded PR in another repository. Tests added for all of them. Existing rule: give each claim a test that fails if it is false

No new AGENTS.md rule: every finding is covered by an existing rule or is one-off.

Copilot review, round 1 (head cca2dfd)

No findings, so nothing changed or was declined. CI on cca2dfd: test and real-docker passed.

Follow-ups: none.

🤖 Generated with Claude Code

mchwang and others added 2 commits October 3, 2026 20:16
With a runner block, the guarded merge gate now inspects, merges and
polls the task's published pull request: the task's latest opened record
in github.baseBranch, which GitHub must show from the task branch, into
that base, with its marker, as the only open PR from the branch, and not
a draft. github.pullRequest becomes optional with a runner block and
blocks merging when it names another PR; without a runner block it is
required and used as before.

The resolved PR must be the same in every validation pass; admission
re-reads the PR record in the transaction that saves the attempt, and the
attempt pins its PR so reconciliation and queue polls never re-resolve
it. The pre-merge already-fixed check treats every PR the task recorded
in the repository as its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

It changes irreversible merge targeting and concurrent reconciliation across storage, GitHub, and queue lifecycles.

Review effort: Balanced
Findings: None

What changed in this PR

Targets guarded merges at each runner task’s published pull request and pins that target throughout validation, submission, and reconciliation.

Changes:

  • Resolves and validates task-published PRs, including draft and branch checks.
  • Persists the target PR on merge attempts and scopes gateway caching/polling accordingly.
  • Adds regression coverage and updates merge documentation.
File Description
README.md Documents runner-managed merge targets.
docs/​implementation/​guarded-merge.md Defines target selection and pinning.
docs/​implementation/​merge-queue.md Documents queue target persistence.
docs/​implementation/​pull-request-opening.md Connects publishing with merging.
github/​merge.ts Adds target-aware inspection, merging, caching, and polling.
github/​pull-requests.ts Exports the opening marker helper.
github/​validate.ts Centralizes base-branch validation.
runner/​merge.ts Resolves, validates, and pins task PRs.
runner/​production.ts Re-exports shared branch validation.
runner/​publish.ts Uses the shared marker helper.
runner/​review.ts Records runner-block presence in configuration.
runner/​store.ts Persists and guards merge-attempt PR targets.
test/​browser/​review.spec.ts Updates browser gateway fixtures.
test/​merge-task-pr.test.ts Adds task-PR targeting regressions.
test/​merge.test.ts Updates merge-state fixtures.
test/​runner-shutdown.test.ts Updates shutdown merge fixtures.
web/​server.ts Selects runner-published versus configured PR mode.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

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.

2 participants