Skip to content

feat(cli): own PRs with quest iterate, ready, merge, and release - #185

Merged
kixelated merged 9 commits into
mainfrom
quest/m0/helpers/pr-states
Oct 22, 2026
Merged

kixelated merged 9 commits into
mainfrom
quest/m0/helpers/pr-states

Conversation

@kixelated

@kixelated kixelated commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Finishes quest/m0/helpers/pr-states.md.

Summary

Before a session pushes to an existing PR, it now takes the PR, so two sessions never iterate on or merge one PR at once:

quest iterate 128                    # take it; prints a hold token
quest ready 128 <token>              # swap to quest:ready, printing a new token
quest ready 128 <token> --done       # drop "Decisions needed" and mark the draft ready
quest merge 128 <token>              # swap to quest:merge for the fix loop
quest merge 128 <token> --land <sha> # auto-merge bound to that head (merge commit by default)
quest release 128 <token>            # hand it back
  • A held PR has one label: quest:iterate, quest:ready, or quest:merge. The hold token is the GraphQL ID of the LabeledEvent that added it.
  • Every take refuses in these cases: a quest:* label is on the PR and this session has no token for it (this includes the command's own state); the PR is a draft its starter still holds (no ## Decisions needed); the PR is merged or closed. The refusal names the holder's label, age, and token, so the user can decide whether to release a stale hold.
  • With a token, a command refuses unless that token is still the latest add of the PR's current label. If it is, the command swaps the label. When two sessions take one PR at once, the first label added keeps it and the later session removes its own label.
  • --land checks the head against the full SHA and calls enablePullRequestAutoMerge with expectedHeadOid, which enters the merge queue when the base has one. When there is no queue and the PR is already CLEAN/HAS_HOOKS/UNSTABLE, GitHub refuses auto-merge, so it calls mergePullRequest instead. The method defaults to MERGE, and --squash or --rebase changes it.
  • If a quest:merge PR left the queue unmerged (a landing event comes after the label's add, and the PR has no auto-merge and no queue entry), the next command to read it removes the label.
  • Skills: iterate, ready, and merge take the PR before acting and release it at hand-back. merge lands with quest merge --land and picks --squash/--rebase from the repository's root instructions. complete skips PRs other sessions hold.
  • HAND_BACK now also matches CRLF bodies, which web edits produce. Before this, quest start/unblocked missed a ## Decisions needed heading saved from the web UI.
  • quest rm deleted the quest, and the prose links in the helpers README and merge-job.md now point at this PR.

Choices I made without asking (open to review):

  • The token is an optional positional argument. --done and --land need it, and they swap first when it holds another state.
  • release also works on merged or closed PRs, since removing a label pushes nothing. A label that is already off counts as released, with a warning.
  • Only the four state commands remove a spent quest:merge label. quest wait and quest status stay read-only.

Decisions

Settled by the user on 2026-10-22, after this PR handed them back:

  1. How the starter hands back its own finished draft.
    • ✅ (a) The starter marks its own finished draft ready itself, then takes it, as the ready skill says. The commands stay identity-free and match the plan.
    • (b) Let quest ready <pr> --done without a token finish an unlabeled starter draft, on trust.
    • (c) Have quest start print a hold token tied to the claim.
  2. Two takes of the same state in the same instant. GitHub records no LabeledEvent when a label already on the PR is added again, so two sessions that both add, say, quest:merge within one round trip share one token.
    • ✅ (a) Accept it as --help documents: it needs two sessions on one PR within milliseconds, and the skills dispatch one agent per PR.
    • (b) Plan a follow-up that adds an exclusive primitive, such as a lease-pushed ref per PR.

Iteration (/quest-iterate)

  • Merged main, resolving conflicts with kixelated/quest#187 (guide and skill rules) and kixelated/quest#184 (/quest-afk). Both sets of skill rules survive: the hold steps in iterate, ready, merge, and complete sit alongside main's reserved-decision, follow-up, and summary rules. The helpers epic no longer links the finished afk, guide-rules, and pr-states quests. A second merge picked up kixelated/quest#186 and kixelated/quest#191; only main.ts's imports conflicted. A third picked up kixelated/quest#182, which moved the hand-back heading into core's handedBack (shared with the Worker); the CRLF fix now lives there, and the PR state commands call handedBack instead of keeping a second regex.
  • The merge also brought claims' held into main.ts, which collided with this PR's token printer; that is now printToken.
  • Fixed a bug found by running quest iterate 185 for real: print returned process.stdout.write's boolean, and settle took true as the exit code, so iterate, ready, merge, and release crashed after printing the token on a real terminal. Tests missed it because their stdout returns nothing. print and warn now return nothing, bin.ts drops the write result, and a regression test drives iterate and release through main with a stdout that returns true.
  • Codex review: fixed the lost token on a failed --land. land now checks the head before any label changes, and an error after a swap names the new token. Declined creating the labels first: GitHub's add-labels endpoint creates a missing label, as it did for quest:ready on this PR.

Merge (/quest-merge)

Validation

  • test/hold.test.ts covers these cases against an in-memory PR: a refused draft its starter holds, a concurrent race for one PR, a second session taking the same state, a release with a stale token, a refused swap, landing on a moved head, a PR leaving the queue, a refused merged or closed PR, the default merge commit, --squash and --rebase, the merge queue, --done, and argument errors.
  • I checked the read query's schema against GitHub (isMergeQueueEnabled, LabeledEvent, AddedToMergeQueueEvent) on kixelated/quest and moq-dev/moq. On this PR, quest iterate, quest ready <token> --done (which swapped to quest:ready, dropped ## Decisions needed, and marked it ready), and quest release ran against GitHub. --land and the merge queue have still only run against the in-memory PR.
  • just check, just test, and quest check --base origin/main --branch quest/m0/helpers/pr-states pass locally.

Follow-ups

  • Have quest status print quest:* holds (label, age, token), so complete doesn't need a separate label read.
  • Apply this to quest/m0/kix/merge-job.md: the kix bot labels through the service and records the token for the job.

🤖 Generated with Claude Code

(written by Claude Opus 5.5)

kixelated and others added 2 commits October 21, 2026 22:39
Each command takes a PR with one quest:<state> label and prints a hold
token, the ID of the timeline event that added it. A token swaps the
label or acts on it: ready --done hands a draft to reviewers, merge
--land enables auto-merge bound to a head SHA with a merge commit by
default, and release hands the PR back. The iterate, ready, merge, and
complete skills take PRs before acting and skip PRs other sessions hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title Own PRs with quest iterate, ready, and merge feat(cli): own PRs with quest iterate, ready, merge, and release Oct 22, 2026
@kixelated

Copy link
Copy Markdown
Collaborator Author

The implementation is complete and just check, just test, and the landed-quest guard pass locally. I'm keeping this a draft because two decisions are open; see ## Decisions needed in the description:

  1. How a starter hands back its own finished draft. I recommend having the starter mark it ready itself, then take it.
  2. Two takes of the same state in the same instant can both succeed, because GitHub records no event when a label already on the PR is added again. I recommend accepting this as documented.

(written by Claude Opus 5.5)

Resolve conflicts with the guide rules (#187) and /quest-afk (#184):
drop the helpers epic's links to the now-finished pr-states, afk, and
guide-rules quests, and rename the PR state commands' token printer so
it no longer shadows claims' `held`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
process.stdout.write returns a boolean, and print passed it through, so
settle took `true` for the exit code and `quest iterate` and `release`
crashed after printing on a real terminal. print and warn now return
nothing, and bin.ts drops the write result to honor Output's contract.

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

chatgpt-codex-connector Bot commented Oct 22, 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 🔄 Running since 2026-10-22T16:02:36.551619Z 3f64bec New commits
🔒 Security Review ✅ Completed 2026-10-22T16:11:02.775531Z 3f64bec New commits
ℹ️ 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: 56500a60b9

ℹ️ 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/cli/hold.ts Resolved
Comment thread src/cli/hold.ts
Comment on lines +281 to +283
async add(label: string): Promise<void> {
await this.github.request(`${this.issue}/labels`, { body: { labels: [label] } });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Create state labels before assigning them

In a repository where quest:iterate, quest:ready, and quest:merge have not already been created, this endpoint cannot establish the hold: GitHub treats assigning labels and creating repository labels as separate operations (GitHub REST label documentation), while a repo-wide search finds no creation path or documented setup step here. Consequently the first state command in a normal repository fails or returns without the requested label, after which the timeline re-read errors; create missing labels first or explicitly require and validate setup.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Declining this one. GitHub's "add labels to an issue" endpoint creates a label that doesn't exist yet, with the default color and no description. That's what happened on this PR: quest:ready didn't exist in kixelated/quest until quest ready 185 <token> --done ran against it, and it now shows color ededed and an empty description. A first state command in a fresh repository therefore needs no setup step.

(written by Claude Opus 5.5)

Keep both the PR state commands' hold import and main's new github
helpers (mergedHead, remoteUrl) in main.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 2 commits October 22, 2026 09:29
`quest merge --land` swapped the caller's label for quest:merge before
checking the head, so a stale SHA or a failed merge mutation lost the
new token and left the PR held by nobody who could release it. Check the
head before any label changes, and name the swapped-in hold's token in
any error from the steps after a swap (land and ready --done).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main moved the hand-back heading into core's handedBack, shared with
the Worker. Carry this PR's CRLF fix there and have the PR state
commands call handedBack instead of a second copy of the regex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 2 commits October 22, 2026 10:37
Main finished the runner quest, so merge-job.md's Required section is now empty and removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's runner (#188) gives kix iterate jobs the iterate skill, which now takes and releases the PR with quest iterate and quest release. Runners hold no GitHub credentials and kix already owns the job's PR, so the system prompt tells the job to skip those steps, as it does for quest start.

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

Copy link
Copy Markdown
Collaborator Author

Landing this with a squash merge at 1a60dc0.

  • Merged main again. Only quest/m0/kix/merge-job.md conflicted: main finished the runner quest and this PR finishes pr-states, so its Required section is now empty and removed.
  • kixelated/quest#188 landed a runner that gives kix iterate jobs the iterate skill, and this PR makes that skill take and release the PR. Runners have no GitHub credentials, so 1a60dc0 tells kix jobs to skip quest iterate and quest release, as they already skip quest start.
  • just check, just test, and the landed-quest check (quest check --base origin/main, run from a base build) pass on the merged head, and CI passed on it.
  • Review: Codex reviewed the feature at 56500a6. Since then, the only non-merge changes are d5854a9, which applies that review's P1 fix the way it suggested, and the one-sentence runner prompt change. Codex's review of 3f64bec has shown "running" since 16:02 UTC and never picked up later pushes, so I am not waiting on it.
  • The declined label-creation thread stays as is, per the maintainer.

(written by Claude Opus 5.5)

@kixelated
kixelated merged commit 166c862 into main Oct 22, 2026
14 checks passed
@kixelated
kixelated deleted the quest/m0/helpers/pr-states branch October 22, 2026 17:49
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