Skip to content
Merged
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 .agents/skills/quest-review/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: quest-review
description: Review a PR's branch locally with a model that wrote none of it, then mark it ready for review.
description: Review a PR's branch locally with a model other than its latest writer, then mark it ready for review.
---

Run `quest skill review` and follow its output.
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/quest-review/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: quest-review
description: Review a PR's branch locally with a model that wrote none of it, then mark it ready for review.
description: Review a PR's branch locally with a model other than its latest writer, then mark it ready for review.
allowed-tools: Bash(quest skill review) PowerShell(quest skill review)
---

Expand Down
21 changes: 12 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1210,11 +1210,11 @@ whole branch at the PR's size.

## Review locally

`quest review` has a model that wrote none of the code review the checkout's
`quest review` has a model other than the latest writer review the checkout's
committed branch before it is pushed for review, at an effort set by the PR's
size. Each size ranks its reviewers, the same for every repository:

| PR size | Reviewer | When Sol wrote the code |
| PR size | Reviewer | When Sol changed it last |
|---|---|---|
| XS | Codex `gpt-6.1-sol` low | Claude Code `claude-opus-5-5` low |
| S | Codex `gpt-6.1-sol` medium | Claude Code `claude-opus-5-5` low |
Expand All @@ -1226,14 +1226,17 @@ A quest branch takes its quest's size, read from the base branch's tip so a
change cannot lower its own review; any other PR, such as a skill's own, takes
the `Size:` line of the `## Models` record in its open PR's body, and a PR with
neither gets M's reviewers. `--size` overrides both. The first reviewer whose
model wrote none of the code reviews: the record's `Implementer:` lines name
the models that did, or `--exclude-model` names each instead. Unless the record
names every one, with no `unknown` model, the harness running the command is
taken to have written it too, so Codex reviews from Claude Code and Claude Code
from Codex. `--harness` names the reviewer instead, at its entry's model and
model is not the latest writer's reviews: the record's last `Implementer:` line
names it, since each session moves its line last whenever it pushes, or
`--exclude-model` names each model to avoid instead. Earlier writers may review.
Without a record, or when its last line names an `unknown` model, or when the
body's and kix's sessions comment's last lines name different models, whose
order cannot be told, the harness running the command, usually the session that
just pushed, is taken to be the latest writer, so Codex reviews from Claude Code
and Claude Code from Codex. `--harness` names the reviewer instead, at its entry's model and
effort, and `--model` and `--effort` override those, as a repository whose
`AGENTS.md` names other reviewers needs. An unknown harness, a size whose
every reviewer wrote the code, or a missing or logged-out CLI, is an error
every reviewer is avoided, or a missing or logged-out CLI, is an error
rather than a same-model review, as is a login the API rejects mid-round, such
as an expired or revoked token. The reviewer reaches its model as the user's own CLI does:
Claude Code keeps `ANTHROPIC_BASE_URL`, `ANTHROPIC_CUSTOM_HEADERS`, and its
Expand Down Expand Up @@ -1742,7 +1745,7 @@ arrives with the guide sections and other skills it builds on, in one call:
| `quest-start` | Claim a ready quest, complete it, and open a PR. |
| `quest-spawn` | Triage ready quests and send several agents out in parallel. |
| `quest-iterate` | Iterate on an open PR to settle its open decisions, fix CI and review findings, and push. |
| `quest-review` | Review a PR's branch locally with a model that wrote none of it, then mark it ready for review. |
| `quest-review` | Review a PR's branch locally with a model other than its latest writer, then mark it ready for review. |
| `quest-merge` | Queue a PR to land, running `quest-iterate` first when it needs work. |
| `quest-complete` | Decide which open PRs to merge, then queue them. |
| `quest-afk` | Keep starting agreed work while you are away, leaving every decision for your return. |
Expand Down
4 changes: 2 additions & 2 deletions assets/CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ A PR that another session pushed to in the last hour is theirs: report its event
Before each push to a PR's branch, confirm the PR is still open, since a push after it merges restores the deleted branch.
Run the repository's checks locally while the PR is a draft; CI may run only on PRs ready for review.

Before a PR is marked ready for review, and before each push to one that is, a model that wrote none of its code reviews the committed branch locally, as `/quest-review` describes.
Before a PR is marked ready for review, and before each push to one that is, a model other than the one that changed it last reviews the committed branch locally, as `/quest-review` describes.

The PR body's `## Models` section records its size as `Size: M` (its quest's, or for a skill's own PR, as a quest is sized) and one `Implementer: Codex, gpt-6.1-sol, high effort` line per implementing session, added when it first pushes; kix records one that pushes to an open PR in its own comment.
The PR body's `## Models` section records its size as `Size: M` (its quest's, or for a skill's own PR, as a quest is sized) and one `Implementer: Codex, gpt-6.1-sol, high effort` line per implementing session, moved last whenever it pushes; kix records one that pushes to an open PR in its own comment.
Read your model and effort rather than guess, writing `unknown` for any you cannot find: Claude Code knows its model and exports `CLAUDE_EFFORT`, and Codex records `model` and `effort` in the last `turn_context` of `~/.codex/sessions/*/*/*/rollout-*-$CODEX_THREAD_ID.jsonl`.

GitHub gives secrets to workflows on branches pushed to the repository, as quest branches usually are, so keep pull request CI, which runs unreviewed code, read-only: a read-only `GITHUB_TOKEN`, and write credentials only in environments limited to protected branches.
Expand Down
6 changes: 3 additions & 3 deletions assets/skills/review.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: Review a PR's branch locally with a model that wrote none of it, then mark it ready for review.
description: Review a PR's branch locally with a model other than its latest writer, then mark it ready for review.
guide: Execution, Summaries, Questions
skills: merge
---
Expand All @@ -21,8 +21,8 @@ Never alter the repository's harness to get a pass, such as by disabling a tool,
If a `## Review` record that `/quest-merge` counts names the committed head, or its review exemptions cover the changes since the head it names, and it leaves no finding unrefuted, run no new round and go on to pushing and marking it ready below.
If you judge the change too trivial to review, skip the review instead and record the skip and its reason in `## Review`.

Otherwise, review the committed branch with `quest review --base <remote>/<base>`, which picks a reviewer that wrote none of the code, and its effort, by the PR's size; `quest review --help` explains the round.
It reads the implementing models, and on a branch that is not a quest's the size, from the PR body's `## Models` record and kix's sessions comment, so write that record before the first round; a quest branch's size is read from its quest.
Otherwise, review the committed branch with `quest review --base <remote>/<base>`, which picks a reviewer other than the model that changed it last, and its effort, by the PR's size; `quest review --help` explains the round.
It reads the latest implementing model, and on a branch that is not a quest's the size, from the PR body's `## Models` record and kix's sessions comment, so write that record before the first round; a quest branch's size is read from its quest.
Pass `--exclude-model` or `--size` only for a PR whose body cannot hold the record.
Where the repository's root instructions name other reviewers, pass them with `--harness`, `--model`, and `--effort`.
Run it in the foreground; a round takes minutes.
Expand Down
5 changes: 3 additions & 2 deletions cloudflare/src/github/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2239,8 +2239,9 @@ export class GitHubService {
if (pull?.state !== "open") throw error;
}
}
// A later session's body keeps the sessions that delivered before it in its `## Models` record.
const body = implementers(pull.body ?? "").reduce(withModels, input.body);
// A later session's body keeps the sessions that delivered before it in its `## Models` record,
// with its own last, since the last names the latest writer.
const body = [...implementers(pull.body ?? ""), ...implementers(input.body)].reduce(withModels, input.body);
if (body.length > BODY_LIMIT) throw new GitHubError("The pull request body is too long");
if (pull.title !== input.title || pull.body !== body)
pull = await this.#api<PullHead>(token, `${pulls}/${number(pull.number)}`, "PATCH", {
Expand Down
11 changes: 7 additions & 4 deletions cloudflare/test/publish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1110,10 +1110,13 @@ describe("runner publication", () => {
await send(one);
await send(one);
await send(two);
const sessions = project.pulls[0]!.comments!.filter((c) => c.author === "quest-test[bot]");
expect(sessions.map((c) => c.body)).toEqual([
`<!-- quest:sessions -->\nThe sessions that published to this pull request after it opened.\n\n## Models\n\n- ${one}\n- ${two}\n`,
]);
const sessions = () => project.pulls[0]!.comments!.filter((c) => c.author === "quest-test[bot]");
const record = (...lines: string[]) =>
`<!-- quest:sessions -->\nThe sessions that published to this pull request after it opened.\n\n## Models\n\n${lines.map((l) => `- ${l}\n`).join("")}`;
expect(sessions().map((c) => c.body)).toEqual([record(one, two)]);
// A session that pushes again moves to the end, so the last line names the latest writer.
await send(one);
expect(sessions().map((c) => c.body)).toEqual([record(two, one)]);
// The body, which holds the author's own review record, is untouched.
expect(project.pulls[0]!.body).toBe(body);

Expand Down
3 changes: 2 additions & 1 deletion cloudflare/test/seed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,9 @@ it("publishes jobs' work end to end, through the upstream branch to the pull req
head: first.head,
pull: models("- Size: S\n- Implementer: opencode, second, high effort"),
});
// The delivering session's own line stays last, since the last names the latest writer.
expect(rocket.pulls.at(-1)!.body).toBe(
models("- Size: S\n- Implementer: opencode, second, high effort\n- Implementer: claude, first, high effort")
models("- Size: S\n- Implementer: claude, first, high effort\n- Implementer: opencode, second, high effort")
.body,
);

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ a fork, already exists. `quest unblocked --unclaimed` lists the ready quests
nobody holds. kix.quest records each active job in its database and reassigns a
job whose runner goes offline; check it for a live runner lock before starting.

You review each change: `/quest-review` has a model that wrote none of it review a
You review each change: `/quest-review` has a model other than its latest writer review a
draft locally and marks it ready, and `/quest-merge` (`$quest-merge` in Codex)
queues it to land once a review covers its head and checks pass.
`/quest-complete` queues a writer's PR that changes no user-facing behavior,
Expand Down
6 changes: 5 additions & 1 deletion quest/m0/model-routing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,12 @@ are mostly judgment.
models: Opus writes the code, since it does a much better job. XS: Grok
high, then Sol medium. S: Opus medium, then Sol high. M: Opus high, then
Sol xhigh. L and XL: Opus xhigh, no fallback. Reviews: Sol low, medium,
high, xhigh, xhigh for XS to XL, never the model that wrote the code;
high, xhigh, xhigh for XS to XL, never the model that changed it last;
Pullfrog and CodeRabbit add free reviews on top.
- Revised 2026-11-07 (user): a review avoids only the latest writer, the
record's last Implementer, which each session moves last whenever it pushes.
Avoiding every writer left no reviewer once Sol and Opus had both written a
PR, and agents then picked an unlisted model themselves.
- Every PR records its models and efforts, and every fix links the PR that
caused it, so the rankings can be audited (user). The PR record alone,
without commit trailers (user, 2026-11-04 design checkpoint of
Expand Down
19 changes: 10 additions & 9 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1043,15 +1043,16 @@ export function main(argv: string[], out: Output): number | Promise<number> {
).join("; ");
program
.command("review")
.summary("Have a model that wrote none of the code review the committed branch, read-only")
.summary("Have a model other than the latest writer review the committed branch, read-only")
.description(
"Review the checkout's committed branch against its base with a reviewer its size picks, never a model " +
`that wrote the code. Each size ranks its reviewers (harness, model, effort): ${reviewers}. The first ` +
"whose model wrote none of the code reviews. The models that wrote it are --exclude-model's; without " +
"it, they are those the Implementer lines of the `## Models` record in the body of the branch's open PR, and of kix's sessions comment on it, " +
"name and, unless those name every one with no `unknown` model or --harness names the reviewer, any " +
"model of the harness running this command, detected from the variables each sets, so Codex reviews " +
"from Claude Code and Claude Code from Codex. " +
"Review the checkout's committed branch against its base with a reviewer its size picks, never the " +
`model that changed it last. Each size ranks its reviewers (harness, model, effort): ${reviewers}. The ` +
"first whose model is not avoided reviews. --exclude-model names each model to avoid; without it, the " +
"latest writer is avoided: the model the last Implementer line names in the `## Models` record of the body of the branch's open PR " +
"and of kix's sessions comment on it. When they name none, an `unknown` model, or two different models, " +
"it is any model of the harness running this command, usually the session that just pushed, detected from the " +
"variables each sets, so Codex reviews from Claude Code and Claude Code from Codex; --harness instead avoids " +
"every model they name. " +
"--harness names the reviewer instead, at its entry's model and effort, and --model and --effort " +
"override the model and effort; a repository whose AGENTS.md names other reviewers passes them. The " +
"size is --size, else the size of the quest a quest/ branch names, read from the base's tip so a " +
Expand Down Expand Up @@ -1116,7 +1117,7 @@ export function main(argv: string[], out: Output): number | Promise<number> {
.option("--effort <level>", "Reasoning effort the reviewer runs at instead")
.option(
"--exclude-model <model>",
"A model that wrote the code, never picked to review it, instead of the PR's record; repeat for each",
"A model never picked to review it, instead of the PR's latest writer; repeat for each",
(value: string, previous: string[] = []) => [...previous, value],
)
.addOption(
Expand Down
4 changes: 2 additions & 2 deletions src/cli/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ export const WRITERS: Record<Size, readonly Model[]> = {
};

/**
* Each PR size's reviewers, ranked: the first whose model wrote none of the
* code reviews it, so Opus reviews what Sol wrote, at its own write efforts.
* Each PR size's reviewers, ranked: the first whose model is not the latest
* writer's reviews it, so Opus reviews what Sol changed last, at its own write efforts.
*/
export const REVIEWERS: Record<Size, readonly Model[]> = {
XS: [sol("low"), opus("low")],
Expand Down
8 changes: 5 additions & 3 deletions src/cli/review-record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { type Comment, recorded, sessionsComment } from "../github/models";
import { type ClaimDependencies, repositoryPath, upstream } from "./claims";
import { git } from "./git";
import { requireRemote, string } from "./github";
import { type Round, located, parseBase } from "./review";
import { type PullModels, type Round, located, parseBase } from "./review";

const HARNESSES: Record<Round["reviewer"], string> = { claude: "Claude Code", codex: "Codex" };

Expand Down Expand Up @@ -101,7 +101,7 @@ export async function pullModels(
baseRef: string,
branch: string,
deps: ClaimDependencies,
): Promise<ReturnType<typeof recorded> | null> {
): Promise<PullModels | null> {
const pull = await openPull(root, baseRef, branch, deps);
if ("skipped" in pull) return null;
// Sessions that published to the PR after it opened are on kix's sessions comment rather than in the body.
Expand All @@ -110,7 +110,9 @@ export async function pullModels(
)) as Comment[];
const own = recorded(pull.body);
const later = recorded(sessionsComment(comments)?.body ?? "");
return { sizes: own.sizes, implementers: [...own.implementers, ...later.implementers] };
// Each records its sessions in the order they last pushed, but which record a later push went to cannot be told.
const latest = [own, later].flatMap((record) => record.implementers.at(-1)?.model ?? []);
return { sizes: own.sizes, implementers: [...own.implementers, ...later.implementers], latest };
}

/** Record `round` in the `## Review` section of its branch's open PR. The edit is the token's user's, which the land gate counts only when that user wrote the PR. */
Expand Down
Loading
Loading