Skip to content

Land picked PRs through merge jobs - #209

Merged
kixelated merged 16 commits into
mainfrom
quest/m0/kix/merge-job
Oct 24, 2026
Merged

kixelated merged 16 commits into
mainfrom
quest/m0/kix/merge-job

Conversation

@kixelated

@kixelated kixelated commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Picking merge on a pull request that is not clean now starts a merge job on one of the maintainer's runners. The job runs the merge skill's loop (fix conflicts, failing checks, and review findings), then lands the PR through GitHub. It holds the PR with the same quest:merge label that local quest merge sessions use. This finishes and deletes quest/m0/kix/merge-job.md.

Changes

  • Workflow. merge joins plan, start, iterate, and bootstrap. Migration 0019_merge_jobs.sql rebuilds jobs for the new CHECK value (keeping idea and context from 0018_plan_request.sql) and adds a hold column. Dependent rows are kept, which I checked against a populated database with foreign keys on. Like iterate, a merge job works on the PR's own head, is untrusted, and is refused on drafts.

  • Hold (label contract). When a merge job is created, the bot adds quest:merge and stores the timeline event ID that added it as the job's hold. The pick is refused while another quest:* label holds the PR, and the refused job is cancelled. When two sessions label at the same moment, the earlier label wins, as in quest merge. A spent quest:merge (a landing that already ended unmerged) is removed first.

  • Release. The bot removes its own label when:

    • a merge job ends with no landing in progress (failed, cancelled, or stopped), or
    • a pull_request webhook arrives and GitHub shows the landing ended unmerged.

    Either way GitHub is reread first; the webhook body is never trusted. The PR then shows as an idle decision item again (src/jobs/landing.ts).

  • Landing. A runner publication can now carry land: { method? }. kix checks that the project is enabled, the published head, that the job still holds its label, and that the PR is open. It then enables auto-merge bound to that head, which enters the merge queue when there is one. If GitHub would refuse auto-merge because nothing is left to wait for, kix merges right away. The method is the one the job names, or else the first the repository allows. A refusal, a moved head, or a removed label pauses the job on a publication decision.

  • Merge commits. Conflicts need a real merge commit, so a publication can name merge: <sha>. kix accepts it only if the PR's upstream base branch already contains that commit. It then writes a two-parent commit, with changes applied relative to the merged commit's tree. On the runner, the workspace detects the agent's git merge and sends only what differs from the merged commit. The workflows refusal compares the merge commit's .github/workflows tree with the head it updates, so a PR's own workflow edits do not block it. This also fixes conflict resolution for iterate jobs: before, publishing a resolved merge as a single-parent commit left GitHub showing the conflict again.

  • Runner.

    • The merge job's prompt comes from the merge skill, with quest merge --land mapped to deliver and quest wait mapped to a new wait tool.
    • wait rereads the PR's context through kix every minute. It returns when the state, head, overall checks, reviews, comments, or threads change, or after at most 55 minutes. The Claude adapter sets the kix MCP server's tool timeout past that, since HTTP tool calls otherwise time out after 60s.
    • deliver takes an optional merge method.
    • Iterate and merge prompts now say to resolve conflicts with git merge, never by rebasing.
  • Inbox. On an open, non-draft PR that is not clean, the merge button starts a merge job. A clean PR keeps the direct merge bound to its exact head. Observations now record a PR's quest:* label, and the inbox lists such a PR as in progress with no actions.

  • Docs. cloudflare/README.md and the new "Merge jobs" section of cloudflare/GITHUB.md.

Choices made without asking

  • Label timing. The label goes on when the job is created, not when a runner is assigned. That way the pick claims the PR right away, and a local session cannot take it while the job is queued.
  • Spent labels. A spent quest:merge is treated as released and removed, matching what quest merge does when it takes a PR.
  • Job ends after landing. The job completes once its landing starts and does not wait for the merge. If the PR leaves the queue unmerged, it goes back to the maintainer. It is not merged again automatically.
  • Merge commits only on PRs. A merge commit is accepted only on a PR job (iterate or merge). A job's own fork branch starts from the default branch, so it has nothing to merge in.
  • Drafts. Drafts are not offered merge and are refused by the API. The merge skill also stops on drafts.
  • No quest on the runner. If quest is not installed in the job's environment, the merge prompt asks the agent to review the quest files the PR changes against the quest guide itself, in place of the quest check --base step.

Validation

  • just check and just test pass locally.
  • New Worker tests cover:
    • holding on pick, cancel releasing the label, refusal when a local session holds the PR, and taking over a spent label;
    • drafts refused;
    • the merge commit and the base check;
    • auto-merge versus an immediate merge, and the default method;
    • decisions for a moved head, a removed label, and a GitHub refusal;
    • release on failure, and release once a landing ends unmerged (also through the webhook);
    • the inbox's merge-job button and label-held items.
  • New runner tests cover merge-commit snapshots, the merge prompt, and the wait loop.

Limitations

  • Not tried against real GitHub or a live runner yet. The GraphQL landing mutations and the labels timeline match what quest merge already uses.
  • If the PR's head is moved by someone else, the job pauses on a decision, and the only way forward is to stop the job and pick merge again on the new head.
  • A job cancelled by an expired lease releases its label on the next pull_request event on that PR, not right away.
  • The shared fake GitHub (cloudflare/dev/github.ts, from kixelated/quest#213) now models labels, the hold query, both landing mutations, compare, and workflow trees, and the merge job publication tests run against it. It has no merge queue.

Suggested follow-ups

  • Run a merge job end to end on the seeded local server, now that the shared fake models labels and landing.
  • Live-test the workflows refusal with a real PR that touches .github/workflows.
  • Let a paused job take a head someone else moved ("continue from the new head") instead of requiring a new pick.
  • Share the label-contract logic (snapshot, spent, rival) between src/cli/hold.ts and the Worker instead of keeping two copies.
  • Wake waiting jobs from webhooks instead of having the runner poll inside wait.

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 3 commits October 23, 2026 23:03
Quest-Claim: 32e768096e37aab7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge jobs hold their PR with quest:merge, publish conflict resolutions
as merge commits, land through auto-merge bound to the published head,
and release the label when the job ends or the landing ends unmerged.
The runner gains a wait tool, and the inbox offers merge on unclean PRs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review October 24, 2026 07:12
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T19:34:09.075517Z 6213cb4 New commits
🔒 Security Review ✅ Completed 2026-10-24T19:37:51.791308Z 6213cb4 New commits

Security findings

Advisory findings (1)

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5326944d4f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/runner/job.ts Resolved
Comment thread cloudflare/src/github/service.ts Resolved
Comment thread src/runner/workspace.ts Resolved
- Give Claude's kix MCP server a tool timeout past WAIT_LIMIT, so a
  merge job's `wait` outlasts the 60s HTTP first-response limit.
- Refuse landing while the project is paused, like delivery.
- Check a merge commit's workflows against the head it updates instead
  of refusing merge-relative changes that only keep the PR's own
  workflow edits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the overlap with planning (#205) and agent activity (#215),
keeping every feature:

- Renumber 0018_merge_jobs.sql to 0019, after 0018_plan_request.sql, and
  carry its new idea and context columns through the jobs rebuild.
- createJob inserts both the plan request and continues a pull request's
  head for merge jobs as well as iterate.
- The runner's context tool covers plan requests and merge jobs; deliver
  records both a landing and suggested follow-ups.
- Agent cards name merge jobs "Merging".
- Drop the finished activity, planning, and merge-job quests from the
  kix epic.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3101b7ebf9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cloudflare/src/jobs/hub.ts Resolved
kixelated and others added 2 commits October 24, 2026 09:12
Take main's shared adapter environment and stop grace for the Claude
adapter, keeping the kix MCP tool timeout for wait; keep both the wait and
opencode runner tests; drop the finished opencode quest from the kix epic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A stopped attempt whose runner never reports a result is cancelled by a
lease expiring in dispatch, or by a status that omits it. Neither path
ran the merge hold release, so quest:merge stayed on the pull request
until its next webhook. Both now return the merge jobs they cancelled
while holding a pull request, and the runner hub releases those holds.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbe5471fe8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cloudflare/src/jobs/routes.ts Resolved
Comment thread src/runner/wait.ts Resolved
kixelated and others added 4 commits October 24, 2026 09:26
Main's publication tests now run against the shared GitHub fake in
cloudflare/dev/github.ts (#213). Port what merge jobs need into it: pull
request labels and the timeline that added them, the hold query, the
auto-merge and merge mutations, comparing a commit with main, and a
commit's .github/workflows tree. The merge job publication tests now
drive that fake, and pulled jobs' read tokens keep contents: read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A runner status could dispatch a new merge job while kix was still adding
its quest:merge label, so the attempt could run, or even end, before the
job recorded its hold. Dispatch now skips merge jobs without a hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kix reads only the latest 50 reviews and 100 comments, so past those a new
entry replaces an old one and counts stay the same. wait now fingerprints
the entries themselves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep main's merged flag on the fake's pull requests (#217) for both its
REST merge and the merge mutation, and document both meanings of a
listed upstream branch.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb95a27025

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cloudflare/src/jobs/store.ts Resolved
kixelated and others added 2 commits October 24, 2026 09:53
A stop that landed while kix was adding quest:merge cancelled a job with
no hold, so its release did nothing, and the hold recorded afterwards
kept the label on. store.held now records a hold only on a queued job,
and the pick releases the label at once when the job was stopped first.

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

Copy link
Copy Markdown
Collaborator Author

Design checkpoint after four review rounds: the maintainer chose to continue for one more round. The approach stays as it is. Each recent finding was a missed path where a merge job's hold should be released, not a flaw in the approach. This round fixes the remaining race, where a stop arrives while the pick is taking the pull request (4f42425). It also merges main (#208). If Codex finds nothing new on the new head, the PR lands as a squash merge.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 23d2022524

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/runner/workspace.ts Resolved
kixelated and others added 2 commits October 24, 2026 10:21
A publication's changes carry no submodules, and a merge commit's tree
builds on the merged-in commit. A pull request's own submodule bump then
fell out of the merge, and the published commit reverted it. The runner
now lists those submodules with a merge publication, and kix refuses it
on a decision instead of publishing the revert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep #212's sandbox alongside merge jobs: both imports in the Claude
adapter and the attempt, main's systemPrompt with quest merge among the
skipped hold commands and the CONFLICTS instruction after it, the fake
Claude logging the sandbox settings and the kix tool timeout, and both
launch assertions. Take main's sandbox quest line; the merge-job quest
stays deleted. Drop an unused binding from the submodule snapshot test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated merged commit 375c740 into main Oct 24, 2026
12 of 13 checks passed
@kixelated
kixelated deleted the quest/m0/kix/merge-job branch October 24, 2026 19:37
@kixelated

Copy link
Copy Markdown
Collaborator Author

Landed 6213cb4 as a squash merge (the repository allows only squash merges).

What changed during /quest-merge:

Maintainer decisions:

  • At the four-round design checkpoint: continue for one round, not a design change.
  • For the submodule finding: refuse such a merge now. Carrying submodule pointers is a follow-up.
  • Landed without a posted Codex code review of a635121. Codex marked that review complete but posted nothing. Its security review of that head was clean, and every finding it did post is resolved. The final head 6213cb4 adds only a conflict-free merge of main (feat(resolve): merge concurrent quest link-list edits with a git merge driver #219).

Checks on 6213cb4:

  • just check and just test pass locally.
  • Every required CI check passes. The optional native Windows (windows-11-arm) job fails in test/gate.test.ts's load-sensitive headroom test, which this PR does not touch.
  • quest check --base origin/main with a main build reports 34 documents ok.

Follow-ups to plan:

  • Publish a pull request's existing submodule pointers in merge jobs, instead of refusing.
  • A scheduled sweep that releases merge holds missed by any release path.
  • Run a merge job end to end on the seeded server.
  • Live-test the workflows refusal with a real PR.
  • The flaky windows-arm gate test.
  • From the PR body:
    • Continue a paused job from a head someone else moved.
    • Share the hold logic between the CLI and the Worker.
    • Wake waiting jobs from webhooks.

(Written by Claude Opus 5.5)

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

🛡️ Codex Security Review · Automatically triggered

Here are some automated security review suggestions for this pull request.

Reviewed commit: 6213cb40b7

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Comment thread src/runner/job.ts
Comment on lines +590 to +594
const land: Land | undefined = job.workflow === "merge" ? (method ? { method } : {}) : undefined;
const followups = followupsOf(args.followups);
const published = await this.#publish(
typeof args.message === "string" && args.message.trim() ? args.message : subject(text),
pull,
{ pull, land },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛡️ Codex Security Review · Automatically triggered

P1 Badge Security: Require approval after the merge agent changes the head

When a maintainer starts a merge job on an upstream/service-fork PR, the agent consumes attacker-controlled PR discussion and head contents, including repository instruction files. This new path turns any deliver call into land without showing the resulting diff to the maintainer. A lower-privileged writer or commenter can therefore use prompt injection to make the App merge post-click changes the maintainer never reviewed. Hold/head checks validate those new changes rather than stopping them, and GitHub rules help only when they require an effective fresh review. Require explicit approval of the final head/diff before landing.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant