Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ never waited for. Dependabot PRs that only bump patch versions or a lockfile
need no review.

PRs land on `main` only through `quest land` (see the README): queue one with
`quest merge <pr> <token> --land <sha>`, and the host daemon
`quest merge <pr> --land <sha>`, and the host daemon
`quest land serve` runs `just check` and `just test` once per batch through its
configured sandbox. Before each landing, the daemon merges the base into the PR
head with the quest merge driver and verifies the tested tree, then pushes that
Expand Down
68 changes: 35 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ review-rounds 2 -
review stale commented kixelated 2026-10-16T06:14:04Z https://github.com/moq-dev/quest/pull/128#pullrequestreview-5543745302
review current commented chatgpt-codex-connector 2026-10-16T17:57:16Z https://github.com/moq-dev/quest/pull/128#pullrequestreview-5551703054
thread PRRT_kwDOUhuoys6tUUV2 current chatgpt-codex-connector https://github.com/moq-dev/quest/pull/128#discussion_r4297708263 src/cli/gate.ts:115
hold quest:merge LE_example 2026-10-16T17:57:16.000Z https://api.github.com/repos/moq-dev/quest/issues/events/123456 1 hour ago
hold quest:active LE_example 2026-10-16T17:57:16.000Z https://api.github.com/repos/moq-dev/quest/issues/events/123456 1 hour ago
```

Each line starts with its record type; any free text, a check name or a path,
Expand Down Expand Up @@ -429,47 +429,49 @@ failures exit 1.

## Own a pull request

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

```sh
quest iterate 128 # 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 it 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
quest release 128 <token> # hand it back
quest iterate 128 # add quest:active, printing a hold token
quest ready 128 <token> # drop "Decisions needed" and mark it ready
quest release 128 <token> # hand it back
quest merge 128 --land <sha> # auto-merge bound to that head; takes no label
```

A taken PR carries one label, `quest:iterate`, `quest:ready`, or
`quest:merge`. Sessions often share a GitHub account, so taking a label prints
a hold token: the ID of the timeline event that added it. Every command refuses
while a `quest:*` label it holds no token for is on the PR, including its own
state's; a draft its starter still holds, one without a `## Decisions needed`
section; and a merged or closed PR, since a push after the merge brings the
branch back. The refusal names the holder's label, age, and token, so you can
decide whether to release a stale hold. When two sessions add different labels
at once, the first label added keeps the PR and the other backs off.

`--land` needs the full 40-character head SHA and refuses once the head moves,
before swapping labels, so the token you passed stays valid. If a step fails
after a swap, the error names the new token.
A taken PR carries the `quest:active` label while an agent works on it.
Sessions often share a GitHub account, so taking it prints a hold token: the
ID of the timeline event that added the label. Every command refuses while a
`quest:*` label it holds no token for is on the PR, including another
session's `quest:active`; a draft its starter still holds, one without a
`## Decisions needed` section; and a merged or closed PR, since a push after
the merge brings the branch back. The refusal names the holder's label, age,
and token, so you can decide whether to release a stale hold. When two
sessions add labels at once, the first label added keeps the PR and the other
backs off. Older quest versions add `quest:iterate` and `quest:ready`, which
hold a PR like any `quest:*` label; passing their token to `quest iterate`
swaps them for `quest:active`. Their `quest:merge` holds nothing and is
ignored.

`quest merge` takes no label: the head-pinned auto-merge it enables is the
queue signal, so a queued PR shows no hold. It refuses while any session holds
the PR, so release your own hold first. `--land` needs the full 40-character
head SHA and refuses once the head moves.
It enables auto-merge bound to that SHA, which enters GitHub's merge queue
when the base has one, or merges at once when nothing is left to wait for. It
makes a merge commit; `--squash` or `--rebase` picks another method, and a
merge queue applies its own. On a base that requires the `quest land` check, it
queues the PR for [`quest land`](#land-a-batch-with-quest-land) instead.
On an ungated base with auto-merge disabled, it refuses before changing labels
and keeps the caller’s hold token valid. Follow the repository’s own landing
On an ungated base with auto-merge disabled, it refuses. Follow the repository’s own landing
instructions in CONTRIBUTING.md or AGENTS.md, such as its label queue; never
retry with `--direct` unless the user asks. `--direct` permits the usual landing
behavior despite that repository setting.
On an ungated base, it compares the live base against that head and refuses a
head that is behind or diverged: merge the base, rerun the repository's checks,
and land the new head. This is a preflight check; GitHub can still move the base
between that comparison and its head-pinned merge mutation. `quest:merge` stays
on while the PR is queued or auto-merging; once the PR leaves the queue unmerged, the next command removes
it. The commands accept the same PR argument and authentication as
between that comparison and its head-pinned merge mutation. A PR that leaves
the queue unmerged shows why under `landing-stopped` in `quest status`.
The commands accept the same PR argument and authentication as
`quest wait`, and exit 0 when done, 3 when refused, 2 for invalid arguments,
and 1 for other failures.

Expand All @@ -478,7 +480,7 @@ and 1 for other failures.
`quest land serve` runs on a maintainer's host, with one process per repository.
It replaces the Land workflow; landing needs no GitHub Actions.
The base requires the unreported `quest land` check, keeping auto-merge waiting.
Queue an approved head with `quest merge <pr> <token> --land <sha>`.
Queue an approved head with `quest merge <pr> --land <sha>`.

```sh
QUEST_LAND_KEY=/run/credentials/quest-land/key quest land serve --app-id 1039466 \
Expand Down Expand Up @@ -568,7 +570,7 @@ conflicted result, sized to GitHub's 65,536-character comment limit. A maintaine
runs `quest land apply <pr>`, which refuses a moved head, rebuilds and checks that
tree, and prints an unpushed commit and its push command. A resolution too large for
the comment hands back plainly. A handback disables
auto-merge, removes `quest:merge`, and carries the reason marker `quest status`
auto-merge and carries the reason marker `quest status`
reads. Each maintainer pick gets one attempt, spent only when a fixer job blames
the PR: a PR that only shared a broken batch keeps it. A blamed PR whose pick already
spent its attempt hands back. A new maintainer pick resets the attempt. Host provisioning and isolation
Expand All @@ -587,7 +589,7 @@ its pick (twice the default `--timeout`), the daemon comments once on that pick,
naming them, since a required check that never reports would hold it silently. The sandbox checks the tip once, without
bisection: success lands one squash commit per PR. A failure, or a base that
conflicts with the batch's first PR, parks those PRs for the fixer: they keep their
auto-merge, `quest:merge`, and place, get no comment, and sit out batches while its
auto-merge and place, get no comment, and sit out batches while its
job runs, so the rest of the queue keeps landing. The fixer reads the log and blames
the PRs that caused it; the others go back to batches unchanged. When it blames
none, the daemon checks the base alone. A green base leaves every PR to the fixer's
Expand All @@ -600,8 +602,8 @@ fix the base, or there is no fixer, does an issue say the queue is halted. If th
base moves and still fails, the daemon closes that fix and runs the fixer on the
new base. Once a check of a moved base passes, the queue resumes, and the daemon
closes the issue and any fix it no longer needs. A job collected while the queue is halted releases its PRs without checking its own, possibly older, base. The daemon collects a finished fixer job even while a check runs.
Without a fixer, a failure ejects the batch instead: ejection disables auto-merge,
removes `quest:merge`, and comments with the reason and the failing log, bounded by
Without a fixer, a failure ejects the batch instead: ejection disables auto-merge
and comments with the reason and the failing log, bounded by
size. When GitHub refuses one PR's squash merge, only that PR stops: a conflict is
parked for the fixer, a moved head or refusal is ejected, and the rest of the batch
stays queued. `quest status` reads the hidden reason marker. Full logs stay in the state directory: `--state`, or by default systemd's
Expand Down Expand Up @@ -635,7 +637,7 @@ and tree before proceeding to the next PR.

If the PR branch push is refused for permissions, the daemon instead pushes the
tested squash to the base as a fast-forward, leased on the expected parent. It
closes the PR with a comment linking the landed commit and removes `quest:merge`.
closes the PR with a comment linking the landed commit.
Such a fork PR is closed rather than shown as merged by GitHub. A PR branch lease
failure ejects `head_moved`. A locally prepared or API squash tree mismatch logs
CRITICAL and ejects the remaining batch as `tree_mismatch`.
Expand Down
2 changes: 1 addition & 1 deletion assets/skills/iterate.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Before you begin, run `quest guide` and `quest skill grill`, and read their outp

You own this PR until it is landable.
Parse the arguments to determine the PR number. Any other text is the user's feedback on the PR; interpret it against the diff.
Take it with `quest iterate <pr>` before acting, and keep the hold token it prints; `quest iterate --help` explains holds.
Take it with `quest iterate <pr>` before acting, passing a token you already hold for it to keep that hold, and keep the hold token it prints; `quest iterate --help` explains holds.
If it refuses because another session holds the PR, stop and report what it printed; taking over a stale hold is the user's choice.
If you cannot push to the PR's branch, stop and report.

Expand Down
11 changes: 5 additions & 6 deletions assets/skills/merge.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ It never fixes the PR, pushes, or waits on GitHub; `/quest-iterate` does the fix

Parse the arguments to determine the PR number, otherwise resolve it from the current context/branch.
If a draft, stop and offer `/quest-review` instead.
Take the PR with `quest merge <pr>` before acting, passing a token you already hold from another state to swap it, and keep the hold token it prints; `quest merge --help` explains holds.
If it refuses because another session holds the PR, stop and report what it printed; taking over a stale hold is the user's choice.
Hand it back with `quest release <pr> <token>` whenever you stop without queueing it, including to recommend `/quest-iterate` or to ask.
It takes no hold, since it never pushes; `quest merge` refuses to queue while another session holds the PR.
If you hold the PR from another skill in this session, release it with `quest release <pr> <token>` first.

Read the PR's head, checks, merge state, reviews by head, and unresolved threads with one `quest status <pr>`.
Read the PR's head, checks, merge state, reviews by head, unresolved threads, and holds with one `quest status <pr>`.
If a `hold` record shows another session holds the PR, stop and report it; taking over a stale hold is the user's choice.
The head is landable when:

- a `## Review` record the guide counts names it, or the guide's review exemptions cover the changes since the one it names;
Expand All @@ -26,12 +26,11 @@ While its required checks are only pending, report that the pick stands until th
Before queueing, fetch the PR's base branch and its exact head, check out the head outside your checkout, such as a detached worktree in `.tmp/`, and run `quest --root <head-checkout> check --base <remote>/<base> --branch <head-branch>` with a `quest` the PR cannot modify, from a directory outside the head checkout: the head's files are untrusted, and a stale local base can skip the check.
A finding means the PR leaves its quest inaccurate: recommend `/quest-iterate`.

Queue the head with `quest merge <pr> <token> --land <full 40-character head SHA>`.
Queue the head with `quest merge <pr> --land <full 40-character head SHA>`.
It makes a merge commit; add `--squash` or `--rebase` when the repository's root instructions name that method for the PR's base.
On a base that requires the `quest land` check, it queues the PR for `quest land`, whose chain merges and tests against the current base.
On an ungated base with auto-merge disabled, it refuses. Follow the repository’s own landing instructions (for example, its label queue); never retry with `--direct` unless the user explicitly asks.
On an ungated base, it refuses a head that does not contain the current base: recommend `/quest-iterate` to merge the base.
The `quest:merge` label stays on while the PR is queued or auto-merging.

Report once it is queued, and do not wait for it to merge.
Plan the PR's suggested follow-ups as soon as it is queued, as if it lands, rather than waiting for the merge, unless a coordinator such as `/quest-complete` called this skill and batches them.
Expand Down
12 changes: 6 additions & 6 deletions assets/skills/review.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ Before you begin, run `quest guide` and `quest skill merge`, and read their outp
Parse the arguments to determine the PR number, otherwise resolve it from the current context/branch.
If its body lists open decisions or unfinished work, offer `/quest-iterate` first.

Take the PR with `quest ready <pr>`, passing a token you already hold from another state to swap it, and keep the hold token it prints; `quest ready --help` explains holds.
Take the PR with `quest iterate <pr>`, passing a token you already hold for it to keep that hold, and keep the hold token it prints; `quest iterate --help` explains holds.
If it refuses because another session holds the PR, stop and report what it printed; taking over a stale hold is the user's choice.
A draft you started in this session has no `## Decisions needed` section, so it is still yours and the command refuses it: review it without taking it, and take it once you have marked it ready below.
A draft you started in this session has no `## Decisions needed` section, so it is still yours and the command refuses it: review it without taking it.

Work in a checkout of the PR's head branch, checking out the head first when someone else opened it.
Commit everything, then run the repository's local checks, unless the guide's Reviews section says to run no code from the head, and fix any failure.
Expand All @@ -23,8 +23,8 @@ A finding the reviewer keeps against such a refutation is a decision for the use
If you judge the change too trivial to review, skip the review instead and record why.

Rewrite the PR body's `## Review` section after each round, or once for a skip.
Push, then flip the PR to "Ready for Review" with `quest ready <pr> <token> --done`, which also drops its `## Decisions needed` section.
For your own draft, mark it ready for review yourself, then take it with `quest ready <pr>`.
Push, then flip the PR to "Ready for Review" with `quest ready <pr> <token>`, which also drops its `## Decisions needed` section.
For your own draft, mark it ready for review yourself.
Do not wait for online reviewers.

Summarize the review and CI from `quest status <pr>`, then interactively prompt the user with your recommendation:
Expand All @@ -37,5 +37,5 @@ While its required checks or automated reviews are still running, report that in

A background agent that cannot prompt lists these in its report instead.

Hand the PR back with `quest release <pr> <token>` before you ask or report.
To go straight on to `/quest-iterate` or `/quest-merge` in this session, pass the token to its command instead, which swaps the label.
Hand the PR back with `quest release <pr> <token>` before you ask, report, or go on to `/quest-merge`.
To go straight on to `/quest-iterate` in this session, pass the token to its `quest iterate` instead, which keeps the hold.
15 changes: 8 additions & 7 deletions cloudflare/GITHUB.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,12 @@ fixes for conflicts, failing checks, and review findings; a head moved by
anyone else, a design change, and the merge skill's design checkpoint pause it
on a decision.

- The job holds the PR with the `quest:merge` label, under the contract
`quest merge --help` states for local sessions: the bot adds it when the job
is created, and refuses the pick while another `quest:*` label holds the PR.
A `quest:merge` whose landing already ended unmerged is spent, and removed
first. The job keeps the timeline event that added the label as its `hold`.
- The job holds the PR with its own `quest:kix` label, under the contract
`quest iterate --help` states for local sessions: the bot adds it when the
job is created, and refuses the pick while another hold label is on the PR.
The job keeps the timeline event that added the label as its `hold`. Like
local sessions, kix ignores the `quest:merge` label older quest versions
left on queued PRs.
- The job's agent resolves conflicts by merging the base branch with Git. A
publication may name that `merge` commit, which kix accepts only when the
PR's upstream base branch already holds it; kix then publishes a merge
Expand All @@ -195,8 +196,8 @@ on a decision.
merge queue when the base has one, or merges it at once when GitHub would
refuse auto-merge because nothing is left to wait for. It uses the method the
job names, else the first the repository allows. A base that requires the
`quest land` check queues the PR for that gate instead, as `quest merge
--land` does: squash auto-merge whose commit body pins the head. A refusal
`quest land` check queues the PR for that gate instead, as `quest merge`
does: squash auto-merge whose commit body pins the head. A refusal
pauses the job on a decision, in GitHub's words.
- The label stays while the job runs or waits, and while the PR is queued or
auto-merging. When the job ends without a landing in progress, or a
Expand Down
5 changes: 3 additions & 2 deletions cloudflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,9 @@ collaborator. The check runs at creation and again at assignment. Other
heads, including unsigned ones and those kix pushed to, route only to Linux
runners that declare VM builds. A `merge` job runs the merge
skill's loop on an open, non-draft pull request and lands it; it holds the PR
with the `quest:merge` label that local `quest merge` sessions share
(`src/jobs/landing.ts`), as described in [GITHUB.md](GITHUB.md#merge-jobs). A five-minute scheduled sweep retries ended jobs’ hold releases even when a lifecycle callback or GitHub read was missed; a live job, a replacement hold, or an active landing keeps its label. Without a `flake.nix`, the route
with its own `quest:kix` label, which local sessions respect as they do each
other's `quest:active` (`src/jobs/landing.ts`), as described in
[GITHUB.md](GITHUB.md#merge-jobs). A five-minute scheduled sweep retries ended jobs’ hold releases even when a lifecycle callback or GitHub read was missed; a live job, a replacement hold, or an active landing keeps its label. Without a `flake.nix`, the route
offers a `bootstrap` job instead. A paused project gets no new jobs and none are
claimed; resuming it dispatches the jobs that waited. Disconnecting a project
deletes its jobs and their output and tells their runners to stop. Duplicate
Expand Down
Loading
Loading