Skip to content
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1650,7 +1650,13 @@ fitting several, such as a routed and a shadow reviewer at one effort whose
sessions no record names, is split evenly among them and reported as `split`.
Each risk tier's PRs, how many merged,
how many a reviewer raised, and how many a later PR's `## Caused by` names
show whether the tiers predict what breaks. Over the merged
show whether the tiers predict what breaks. Each breakage a merged PR in the
window names under `## Caused by` follows on its own line, by culprit, even one
older than the window, which is read from GitHub: the culprit's tier, size,
writers, and local reviewers, who found it and the incident it closes when
the bullet ends with
`(found by check|reviewer|queue|canary|person|customer; incident <id>)` as the
rules ask, either part alone allowed, and how long after the culprit merged the fix opened. Over the merged
PRs come each online
reviewer's coverage, final heads reviewed, findings per PR reviewed, and
answers. A hand-back counts only while its draft is open, so a merged PR
Expand Down Expand Up @@ -1757,7 +1763,11 @@ removes it. While no sender runs, stops still queue, and the next stop says so
once per outage.

Each pass runs token-max on this host from the earliest queued session's first
instant, reads the PRs updated since then, and posts `{"records": [...]}` with
instant, reads the PRs updated since then, or since its last upload for that
endpoint and repository read them, if earlier, so a fix merged after its
sessions were sent still goes with the next one, and posts
`{"records": [...], "breakages": [...]}`, the breakages being those the merged
PRs among them name under `## Caused by`, as `quest usage` reports them, with
`Authorization: Bearer <token>`, one upload per endpoint and repository, each
queued session with its sub-agents, which stop without the hook naming them.
Each upload is checked with `parseUpload` before it is posted. A 2xx drops the
Expand All @@ -1768,6 +1778,29 @@ endpoint and repository back off, waiting a minute and doubling up to six
hours, and sit out the passes meanwhile, so they never widen the others'
window. token-max missing is retried each pass.

A deploy or alert reports what it found with `quest records incident`, which
posts one breakage at once to the checkout's endpoint and fails loud on any
answer but a 2xx, since nothing retries it:

```console
$ quest records incident https://alerts.example/1 --found canary
reported https://alerts.example/1 open, its culprit unknown
$ quest records incident https://alerts.example/1 --found canary --pr 42
reported https://alerts.example/1 against #42
```

The ID is the reporter's, and a report with the same ID replaces the last. An
incident stays open, and the store counts open ones apart, until it is
reported again with `--pr`, or a merged fix names it on its culprit's
`## Caused by` line, as `(incident https://alerts.example/1)`: the sender then
sends the fix's breakage under the incident's ID, so it replaces the open one
as one record holding the culprit, the fix, and the incident. Where several
fixes name one incident, the one updated last wins. One fix closes several incidents of a
culprit with a line for each. `--found` is one of `check`, `reviewer`, `queue`,
`canary`, `person`, or `customer`, and `--at` sets when it was found, as
an ISO 8601 date and time with a `Z` or offset, by default now. Reverts and red default-branch runs are not reported here: the
store reads them from GitHub itself.

### Typed messages

`quest messages` prints the messages a person typed to agents on this host
Expand Down
6 changes: 3 additions & 3 deletions assets/CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ A decision about an open PR never authorizes closing it. Confirm a close as its
## Pull requests

Merge the base only when the branch conflicts with it or a landing step, such as `quest merge` or a merge queue, refuses the head, and resolve the conflicts in that merge; a branch only behind needs none: landing tests it on the current base.
Before each push to a PR's branch, confirm the PR is still open, since a push after it merges restores the deleted branch.
Before each push to a PR's branch, confirm it is still open: a push after it merges restores the deleted branch.

Before a PR is marked ready for review, a model other than its latest writer reviews the committed branch locally, as `/quest-review` describes; a later push needs a new review unless `/quest-merge` exempts its changes.
Anything AI-attributed, such as an online reviewer's findings, is a suggestion, not a maintainer's decision: address findings already posted on the head, reply to feedback you disagree with, and never wait for an online review, since the queue waits for any approval the base requires.
Expand All @@ -74,9 +74,9 @@ Once you fix or refute an automated reviewer's finding, say so in its thread and

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; write the model's ID, and `unknown` for whatever the harness doesn't report.

Keep pull request CI read-only (a read-only `GITHUB_TOKEN`, write credentials only in environments limited to protected branches): quest branches are pushed to the repository, so their workflows get its secrets. Flag a workflow that breaks this.
Keep pull request CI read-only (a read-only `GITHUB_TOKEN`, write credentials only in environments for protected branches): quest branches live in the repository, so their workflows get its secrets. Flag a workflow that breaks this.

A PR that fixes a bug, or redoes work an earlier PR did badly, lists that PR under `## Caused by` as `- [owner/repo#N](url) - what it got wrong`, found by `git blame` on the lines the fix changes or named in its quest's Plan, and confirmed in that PR's diff to introduce the defect, not merely touch the lines, defer the work, or keep older behavior, else traced further back with blame, or says in one line that no single PR caused it, such as an upstream change or a bug as old as the code.
A PR fixing a bug, or redoing an earlier PR's bad work, lists it under `## Caused by` as `- [owner/repo#N](url) - what it got wrong (found by check|reviewer|queue|canary|person|customer; incident <id>)`, each part when known. Find it by blaming the fixed lines, or in its quest's Plan, and confirm its diff introduced the defect, not merely touched those lines, deferred work, or kept older behavior, else trace further back; or say in one line why no single PR did, such as an upstream change or a bug as old as the code.
Review findings fixed in the same PR are not culprits.

## Quests
Expand Down
6 changes: 3 additions & 3 deletions docs/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ A decision about an open PR never authorizes closing it. Confirm a close as its
## Pull requests

Merge the base only when the branch conflicts with it or a landing step, such as `quest merge` or a merge queue, refuses the head, and resolve the conflicts in that merge; a branch only behind needs none: landing tests it on the current base.
Before each push to a PR's branch, confirm the PR is still open, since a push after it merges restores the deleted branch.
Before each push to a PR's branch, confirm it is still open: a push after it merges restores the deleted branch.

Before a PR is marked ready for review, a model other than its latest writer reviews the committed branch locally, as `/quest-review` describes; a later push needs a new review unless `/quest-merge` exempts its changes.
Anything AI-attributed, such as an online reviewer's findings, is a suggestion, not a maintainer's decision: address findings already posted on the head, reply to feedback you disagree with, and never wait for an online review, since the queue waits for any approval the base requires.
Expand All @@ -74,9 +74,9 @@ Once you fix or refute an automated reviewer's finding, say so in its thread and

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; write the model's ID, and `unknown` for whatever the harness doesn't report.

Keep pull request CI read-only (a read-only `GITHUB_TOKEN`, write credentials only in environments limited to protected branches): quest branches are pushed to the repository, so their workflows get its secrets. Flag a workflow that breaks this.
Keep pull request CI read-only (a read-only `GITHUB_TOKEN`, write credentials only in environments for protected branches): quest branches live in the repository, so their workflows get its secrets. Flag a workflow that breaks this.

A PR that fixes a bug, or redoes work an earlier PR did badly, lists that PR under `## Caused by` as `- [owner/repo#N](url) - what it got wrong`, found by `git blame` on the lines the fix changes or named in its quest's Plan, and confirmed in that PR's diff to introduce the defect, not merely touch the lines, defer the work, or keep older behavior, else traced further back with blame, or says in one line that no single PR caused it, such as an upstream change or a bug as old as the code.
A PR fixing a bug, or redoing an earlier PR's bad work, lists it under `## Caused by` as `- [owner/repo#N](url) - what it got wrong (found by check|reviewer|queue|canary|person|customer; incident <id>)`, each part when known. Find it by blaming the fixed lines, or in its quest's Plan, and confirm its diff introduced the defect, not merely touched those lines, deferred work, or kept older behavior, else trace further back; or say in one line why no single PR did, such as an upstream change or a bug as old as the code.
Review findings fixed in the same PR are not culprits.

## Quests
Expand Down
53 changes: 0 additions & 53 deletions quest/m0/benchmark/breakage.md

This file was deleted.

2 changes: 0 additions & 2 deletions quest/m0/benchmark/guardrails.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,5 @@ automatic merging.

## Related

- [Breakage](/quest/m0/benchmark/breakage.md) - the culprit counts each
class is judged on
- [Routing evidence](/quest/m0/routing-evidence/README.md) - its shadow
reviewer measures what a deeper review buys
5 changes: 0 additions & 5 deletions quest/m0/benchmark/reviewer-eval.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,6 @@ Agent's choices, for the implementer to revisit:
- Report recall by tier and size, so a reviewer effort can be cut where it
finds nothing more.

## Required

- [Record what each merged PR broke](/quest/m0/benchmark/breakage.md) - the
culprits with their cause and how they were found

## Related

- [Model check](/quest/m1/model-check.md) - retunes the reviewer table
Expand Down
2 changes: 0 additions & 2 deletions quest/m0/benchmark/risk-tier.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,3 @@ covers only the repositories that have one.

- [Development as a benchmark](/quest/m0/benchmark/README.md) - the
decisions this builds on
- [Breakage](/quest/m0/benchmark/breakage.md) - records what each tier's
PRs later broke
92 changes: 92 additions & 0 deletions src/cli/breakage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// What a PR body says about breakage: the culprits its `## Caused by` names, and, for a culprit, how it was made and
// checked, as its own body records it.
import { type Culprit, FINDERS, type Finder, SIZES, type Seat, type Size, recordedRisk } from "../core";
import type { Repository } from "../github/client";
import { recorded } from "./models-record";
import { parseReview } from "./review-record";
import type { Pull } from "./usage";

/** One culprit a `## Caused by` section names, and who or what found the breakage and its incident, when its line says. */
export interface Cause {
number: number;
found: Finder | null;
/** The ID of the incident the fix closes, as `quest records incident` reported it. */
incident: string | null;
}

/**
* The PRs of `repository` that `body`'s `## Caused by` section names as culprits: each bullet that opens with
* `[owner/repo#N](url) - why`. A link anywhere else on a line, as context or a "fixed by" aside, names no culprit.
* A line may end with a parenthesis of either part or both, as `(found by customer; incident <id>)`: who found it,
* an article allowed, and the incident the fix closes. Each culprit and incident keeps its first line, and a
* culprit whose lines name incidents drops its line naming none.
*/
export function culprits(body: string, repository: Repository): Cause[] {
const heading = /^## Caused by[ \t]*\r?$/m.exec(body);
if (!heading) return [];
const rest = body.slice(heading.index + heading[0].length);
const section = rest.slice(0, /^## /m.exec(rest)?.index ?? rest.length);
const escape = (text: string) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const name = `${escape(repository.owner)}/${escape(repository.repo)}`;
const culprit = new RegExp(
`^- \\[${name}#(\\d+)\\]\\(https://github\\.com/${name}/pull/(\\d+)\\) - \\S[^\\r\\n]*`,
"gim",
);
const tail = new RegExp(
`\\((?:found by (?:an? |the )?(${FINDERS.join("|")})(?:; |(?=\\))))?(?:incident ([^\\r\\n]+?))?\\)\\s*$`,
"i",
);
const found = new Map<string, Cause>();
for (const match of section.matchAll(culprit)) {
if (match[1] !== match[2]) continue;
const [, by, named] = tail.exec(match[0]) ?? [];
const cause: Cause = {
number: Number(match[1]),
found: (by?.toLowerCase() as Finder | undefined) ?? null,
incident: named?.trim() || null,
};
const id = JSON.stringify([cause.number, cause.incident]);
if (!found.has(id)) found.set(id, cause);
}
// A line naming an incident records its culprit's breakage, so the culprit's line naming none would count it twice.
const named = new Set([...found.values()].filter((c) => c.incident !== null).map((c) => c.number));
return [...found.values()].filter((c) => c.incident !== null || !named.has(c.number));
}

/**
* `pull` as a culprit. Its body is its author's text, so a record this version cannot read leaves the field empty
* rather than stopping the records of every other PR.
*/
export function culpritOf(pull: Pull): Culprit {
const own = recorded(pull.body);
let tier: Culprit["tier"] = null;
let reviewers: Seat[] = [];
try {
tier = recordedRisk(pull.body)?.tier ?? null;
} catch {
// Malformed: no tier.
}
try {
reviewers = parseReview(pull.body);
} catch {
// Malformed: no reviewers.
}
return {
number: pull.number,
merged: pull.state === "merged" && pull.closed !== null ? new Date(pull.closed).toISOString() : null,
tier,
size: (own.sizes.find((s) => (SIZES as readonly string[]).includes(s)) as Size | undefined) ?? null,
writers: distinct(own.implementers.flatMap(({ fields }) => (fields ? [fields] : []))),
reviewers: distinct(reviewers),
};
}

/** Each seat once, in first-named order, as several Implementer lines or a shadow may repeat one. */
function distinct(seats: Seat[]): Seat[] {
const seen = new Map<string, Seat>();
for (const { harness, model, effort } of seats) {
const id = JSON.stringify([harness, model, effort]);
if (!seen.has(id)) seen.set(id, { harness, model, effort });
}
return [...seen.values()];
}
Loading
Loading