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
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ head 0ade9d977d54c6f48152410532c58e9ea78db3d0
mergeable conflicting
merge-state dirty
review-decision -
review-record diff-identical 5f1c2e9a8b7d6c5e4f3a2b1c0d9e8f7a6b5c4d3e
auto-merge -
merge-queue -
landing-stopped -
Expand All @@ -472,6 +473,7 @@ comes last, and a missing value prints as `-`:
| --- | --- |
| `url`, `state`, `draft`, `head` | The PR URL; `open`, `closed`, or `merged`; `true` or `false`; the head SHA. |
| `mergeable`, `merge-state`, `review-decision` | GitHub's values, lowercased. |
| `review-record` | Whether the PR body's [`## Review` record](#review-locally) still covers the head, and the head it names last: `current` when it is the head, `diff-identical` when `git diff <base>...<head>` prints the same as for it, `stale` otherwise, or `unknown` when GitHub cannot compare them, such as a diff too large; `-` when the record names no full SHA. Whose record counts is the reader's call. |
| `auto-merge` | Method, who enabled it, when, and the head it was enabled on. |
| `merge-queue` | Queue state, position, and when it was enqueued. |
| `landing-stopped` | The reason the latest merge-queue entry, auto-merge, or [`quest land`](#land-a-batch-with-quest-land) queue entry ended (such as `merge_conflict`, `failed_checks`, `manual`, `manually_disabled`, or `merged`), who ended it, and when; `-` once the PR has entered the merge queue or auto-merge again since. |
Expand Down Expand Up @@ -1385,18 +1387,18 @@ from the branch into the base, found among the upstream's PRs from the
repository the base's remote pushes to, and prints `record <url>`. The record
names the reviewer harness, model, and effort, the session and round, the
reviewed head, the base's short SHA, the score and rationale, each finding with
its location, and the last round's findings with their dispositions. The land
gate counts every full SHA the section names as a review of that head, so the
its location, and the last round's findings with their dispositions. `quest
status` takes the last full SHA the section names as the reviewed head, so the
head appears in full only once a round reports no findings, and model text
appears as inline code, which mentions or links nothing and never holds a full
SHA. The section runs to the next level-1 or
level-2 heading, both as Markdown parses them, so an example in a code fence is
text to the land gate and the record alike; the rest of the body stays as written, and a body without one
text to `quest status` and the record alike; the rest of the body stays as written, and a body without one
gains it at the end, unless it ends inside an unclosed code fence or HTML
comment, which fails the record. With no such PR, or a remote off GitHub, it changes nothing
and prints `record skipped: <why>`. It edits as the user the GitHub token names
(`GH_TOKEN`, `GITHUB_TOKEN`, or `gh auth token`), and the land gate counts the
record only while the PR's author made the body's latest edit, so review your
(`GH_TOKEN`, `GITHUB_TOKEN`, or `gh auth token`), and `/quest-merge` counts the
record only on a PR whose author has write access, so review your
own PRs with your own token. A record GitHub refuses fails the command after the
round's report, with exit status 1. It exits 0 once the round
finishes, whatever it found, 2 for invalid arguments or a checkout or CLI that
Expand Down
2 changes: 1 addition & 1 deletion assets/skills/complete.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The skill argument can be used to filter the PRs in scope.
Run each `/quest-merge` inline in this session; it queues a landable head, or hands the PR to `/quest-iterate`.
Run each `/quest-review` and `/quest-iterate` in parallel in a sub-agent.

`/quest-iterate` is the default for a PR that needs work: dispatch it without asking when the PR has no decision for the user, such as for a merge conflict, failing checks, unaddressed review findings, or no counted review of the head.
`/quest-iterate` is the default for a PR that needs work: dispatch it without asking when the PR has no decision for the user, such as for a merge conflict, failing checks, unaddressed review findings, or no counted review of the head, which a `review-record` line of `current` or `diff-identical` in `quest status <pr>` already is.
It fixes the PR, reviews it, and marks it ready; ask about the PR again when it reports.
A PR with open decisions, such as those its `## Waiting` section lists or a significant issue you would solve differently, goes to the user instead: settle its decisions with the grill skill's rounds, then dispatch `/quest-iterate` with the answers in its brief.

Expand Down
3 changes: 2 additions & 1 deletion assets/skills/merge.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ A PR that adds follow-ups also needs the answers of the round that picked it rec
While its required checks are only pending, report that the pick stands until they pass; do not wait for them.

A review counts for the head whose SHA it names: a `## Review` record of a local `quest review` round, or a recorded skip.
Changes since that head need no new review only when they are conflict-free merges from the base branch, a reviewer's suggested patch applied unchanged, the fixes of a round whose findings were all low severity, a land fixer's recorded mechanical fix that changes no behavior, the pick round's answers recorded in the PR's follow-ups, or changes, such as a hand-resolved merge, that leave the head's diff identical to the reviewed head's: `git diff <remote>/<base>...<head>` prints the same as `git diff <remote>/<base>...<reviewed-head>`.
`quest status <pr>`'s `review-record` line names that head; `current` or `diff-identical` (the head's diff from the base is unchanged since it) counts, so read it rather than comparing diffs yourself.
Otherwise, changes since that head need no new review only when they are conflict-free merges from the base branch, a reviewer's suggested patch applied unchanged, the fixes of a round whose findings were all low severity, a land fixer's recorded mechanical fix that changes no behavior, or the pick round's answers recorded in the PR's follow-ups.
The repository's review policy decides whose record counts and may require more, such as a maintainer's review.
Where it says nothing, only a record on a PR whose author has write access counts: anyone else writes their own PR body, so review their head locally and rewrite the record before landing it.

Expand Down
2 changes: 1 addition & 1 deletion assets/skills/review.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ On the head of a PR whose author lacks write access to the repository, run no co
A check failure that reproduces unchanged in a checkout of the base branch is pre-existing rather than the PR's: record the command and both failures in the PR body's validation, go on to review and readiness, and add a follow-up for it unless an open PR already fixes it.
Never alter the repository's harness to get a pass, such as by disabling a tool, unsetting its environment, or skipping a check, nor report an altered run as its checks.

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 a `## Review` record that `/quest-merge` counts names the committed head, or `quest status <pr>` reports it `diff-identical` while its `head` line is the committed head, or its other 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 other than the model that changed it last, and its effort, by the PR's size; `quest review --help` explains the round.
Expand Down
24 changes: 0 additions & 24 deletions quest/m0/token-waste/review-carry.md

This file was deleted.

4 changes: 3 additions & 1 deletion src/cli/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export interface RequestOptions {
body?: unknown;
method?: "POST" | "PUT" | "PATCH" | "DELETE";
allow?: number[];
/** A media type other than JSON, such as `application/vnd.github.diff`. */
accept?: string;
/** Overrides how the request repeats; the default follows its method and endpoint. */
repeat?: Repeat;
}
Expand Down Expand Up @@ -253,7 +255,7 @@ export class GitHub {
? options.method && { method: options.method }
: { method: options.method ?? "POST", body: JSON.stringify(options.body) }),
headers: {
Accept: "application/vnd.github+json",
Accept: options.accept ?? "application/vnd.github+json",
Authorization: `Bearer ${this.deps.token}`,
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "quest",
Expand Down
6 changes: 4 additions & 2 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,8 +532,10 @@ export function main(argv: string[], out: Output): number | Promise<number> {
"Print a pull request's head, checks, merge state, reviews, reactions, and unresolved review threads " +
"in one call. It reports facts; it does not decide whether the PR is ready or whose review counts.\n\n" +
"Plain output prints one record per line, its type first and any free text (a check name or path) last: " +
"url, state, draft, head, mergeable, merge-state, review-decision, auto-merge, merge-queue, " +
"landing-stopped, and checks, whose state and counts follow each check's latest run; auto-merge gives its method, who enabled it and when, and the head it was " +
"url, state, draft, head, mergeable, merge-state, review-decision, review-record, auto-merge, merge-queue, " +
"landing-stopped, and checks, whose state and counts follow each check's latest run; review-record gives whether the PR body's ## Review record " +
"covers the head, as current, diff-identical (git diff <base>...<head> prints the same as for the head it names), " +
"stale, or unknown when GitHub cannot compare them, then the head it names last; auto-merge gives its method, who enabled it and when, and the head it was " +
"enabled on, which differs from head once a push follows, as GitHub keeps auto-merge across a writer's " +
"push (- when it cannot be told: a quest land pin names it, and otherwise the head branch's push " +
"activity, which GitHub lists up to minutes late); landing-stopped is the latest exit from the merge queue, auto-merge, or " +
Expand Down
6 changes: 3 additions & 3 deletions src/cli/review-record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const HARNESSES: Record<Round["reviewer"], string> = { claude: "Claude Code", co

/**
* Model text as one inline code span, so a mention, issue reference, or Markdown in it renders as text.
* The land gate counts every full SHA the section names, so no full SHA appears in it.
* `quest status` takes the last full SHA the section names as its reviewed head, so no full SHA appears in it.
*/
function code(text: string): string {
const flat = text
Expand All @@ -32,7 +32,7 @@ export function reviewRecord(round: Round): string {
"",
`- Reviewer: ${HARNESSES[round.reviewer]}, ${round.model}, ${round.effort} effort`,
`- Session: ${code(round.session)}, round ${round.round}`,
// The land gate counts a full SHA here as a review of that head, which an open finding has not passed.
// `quest status` counts a full SHA here as a review of that head, which an open finding has not passed.
`- Head: ${round.findings.length === 0 ? round.head : `${round.head.slice(0, 12)}, named in full once a round reports no findings`}`,
`- Base: ${round.base.ref} at ${round.base.sha.slice(0, 12)}`,
`- Score: ${round.score}/10, ${code(round.rationale)}`,
Expand Down Expand Up @@ -159,7 +159,7 @@ export async function pullModels(
return recorded(pull.body);
}

/** 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. */
/** Record `round` in the `## Review` section of its branch's open PR. The edit is the token's user's, which `/quest-merge` counts only when that user wrote the PR. */
export async function recordRound(root: string, round: Round, deps: ClaimDependencies): Promise<Recorded> {
const pull = await openPull(root, round.base.ref, round.branch, deps);
if ("skipped" in pull) return pull;
Expand Down
63 changes: 62 additions & 1 deletion src/cli/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "./github";
import { checksState, contextOutcome, latestContexts } from "../github/checks";
import { EJECTED, pinned } from "./land";
import { reviewSection } from "../core";
import { readHoldStatus, type HoldFact, type HoldReport } from "./hold";

/**
Expand All @@ -24,7 +25,7 @@ const STATUS = `query($owner: String!, $repo: String!, $number: Int!,
$moreChecks: Boolean!, $moreReviews: Boolean!, $moreThreads: Boolean!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
url state isDraft headRefOid headRefName mergeable mergeStateStatus reviewDecision
url body baseRefName state isDraft headRefOid headRefName mergeable mergeStateStatus reviewDecision
headRepository { nameWithOwner }
autoMergeRequest { enabledAt mergeMethod commitBody enabledBy { login } }
mergeQueueEntry { state position enqueuedAt }
Expand Down Expand Up @@ -102,6 +103,17 @@ export interface Thread {
url: string;
}

/**
* The head the PR body's `## Review` section names last, the latest reviewed one: a round's record, or a line after
* it for low-severity fixes or a skip. `current` is the PR's head; `diff-identical` an earlier head whose diff from
* the base (`git diff <base>...<sha>`) matches the head's; `unknown` one GitHub cannot compare, such as a diff too
* large to render.
*/
export interface ReviewRecord {
state: "current" | "diff-identical" | "stale" | "unknown";
head: string;
}

export interface Status {
url: string;
state: "open" | "closed" | "merged";
Expand All @@ -121,6 +133,8 @@ export interface Status {
checks: { state: string | null; passed: number; failing: Check[]; pending: Check[] };
/** Each reviewer's latest submitted review, oldest first. */
reviews: Review[];
/** Null when the `## Review` section names no full SHA. */
reviewRecord: ReviewRecord | null;
reactions: Reaction[];
/** Unresolved review threads, oldest first. */
threads: Thread[];
Expand Down Expand Up @@ -184,10 +198,53 @@ export async function pullRequestStatus(pr: PullRequest, deps: GitHubDependencie
if (ownership.head !== first!.headRefOid)
throw new Error("the PR head changed while reading its holds; rerun quest status");
const status = summarize(first!, rollup, contexts, reviewNodes, threadNodes);
status.reviewRecord = await reviewRecord(github, pr, first!, deps.warn);
if (status.autoMerge !== null) status.autoMerge.head = await enabledHead(github, first!, deps.warn);
return { ...status, holds: ownership.holds, starterHold: ownership.starter };
}

async function reviewRecord(
github: GitHub,
pr: PullRequest,
pull: ObjectValue,
warn: (message: string) => void,
): Promise<ReviewRecord | null> {
const body = typeof pull.body === "string" ? pull.body : "";
const section = reviewSection(body);
// Only the last SHA, so a body naming many costs no more requests than one.
const reviewed =
section &&
body
.slice(section.start, section.end)
.match(/\b[0-9a-f]{40}\b/g)
?.at(-1);
if (!reviewed) return null;
const head = string(pull.headRefOid);
if (reviewed === head) return { state: "current", head: reviewed };
const base = encodeURIComponent(string(pull.baseRefName));
const diffs: string[] = [];
for (const sha of [head, reviewed]) {
const unknown = (why: string): ReviewRecord => {
warn(`cannot compare ${sha} with the base, so whether the review of ${reviewed} carries is unknown${why}`);
return { state: "unknown", head: reviewed };
};
let response: Response;
try {
response = await github.request(`/repos/${pr.owner}/${pr.repo}/compare/${base}...${sha}`, {
accept: "application/vnd.github.diff",
allow: [404, 406, 422],
});
} catch (error) {
// A deadline is the caller's to handle; a forbidden or failing compare is not.
if (error instanceof GitHubDeadline || !(error instanceof Error)) throw error;
return unknown(`: ${error.message}`);
}
if (!response.ok) return unknown("");
diffs.push(await response.text());
}
return { state: diffs[0] === diffs[1] ? "diff-identical" : "stale", head: reviewed };
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

/** Repository activity types that move a branch to a new head, including merges into it. */
export const BRANCH_UPDATES = ["push", "force_push", "branch_creation", "pr_merge", "merge_queue_merge"];

Expand Down Expand Up @@ -363,6 +420,7 @@ function summarize(
},
checks,
reviews: [...latest.values()],
reviewRecord: null,
reactions,
threads,
holds: [],
Expand Down Expand Up @@ -413,6 +471,9 @@ export function statusLines(status: Status): string[] {
`mergeable ${status.mergeable}`,
`merge-state ${status.mergeState}`,
`review-decision ${dash(status.reviewDecision)}`,
status.reviewRecord === null
? "review-record -"
: `review-record ${status.reviewRecord.state} ${status.reviewRecord.head}`,
status.autoMerge === null
? "auto-merge -"
: `auto-merge ${status.autoMerge.method} ${status.autoMerge.enabledBy} ${status.autoMerge.enabledAt} ${dash(status.autoMerge.head)}`,
Expand Down
2 changes: 1 addition & 1 deletion src/core/review.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// A PR body's `## Review` record, which `quest review` writes and the land gate reads.
// A PR body's `## Review` record, which `quest review` writes and `quest status` reads.

import { markdown } from "./doc";

Expand Down
Loading
Loading