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: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -579,9 +579,15 @@ runs `quest land apply <pr>`, which refuses a moved head, rebuilds and checks th
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 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
reads. Each failure a fixer job blames a PR for is a round of its maintainer pick; a PR
that only shared a broken batch has none. The fixer keeps going while each round
changes something, and a handback names the stop rule that ended it: a round with no
new commit for a failure that isn't flaky, such as a conflict requeued unchanged; the
failure a fix addressed recurring right after it; or three flaky rounds in a row,
where the fixer requeued a PR unchanged and the base passed alone. The fixer names
each failure's test or step, else the round counts the gate's check as one; a
conflict names the base it conflicts with, so one after the base moved is new. A new
maintainer pick starts a fresh history. Host provisioning and isolation
belong to the host; these commands are executable paths, not shell expressions.
Wrappers must expose only the supplied tree and log, never the state directory
or its private bare repository.
Expand Down
9 changes: 8 additions & 1 deletion assets/land-fix.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ PR's.
You start on `base`, the batch's base. The repository also has `batch`, the
failed merge, and one `pr-<number>` branch per supplied PR at its pinned head.
For each PR, requeue it unchanged if healthy (if the failure was flaky, name the
flaky test in the reason so it can be followed up), commit a
flaky test in its `failure` so it can be followed up), commit a
mechanical fix on its own branch, or hand it back with diagnosis and recommendation.
Allowed fixes: preserve both sides' intent in conflicts, format/lint, regenerate
lockfiles/generated files, or follow a base rename. Merge base where practical.
Expand Down Expand Up @@ -54,6 +54,13 @@ one entry per supplied PR, no extra fields. Use one of:
{"number":123,"action":"fixed","head":"full 40-character SHA","reason":"one-line mechanical reason"}
{"number":123,"action":"handback","diagnosis":"one line","recommendation":"one line"}

On each entry, add `"failure"`: the test or step the failure log shows
failing, on one line of at most 200 characters, such as
`"failure":"cargo test -p moq-relay: interop::render_timeout"`. Name it the
same way each time the same test or step fails, from the log rather than from
your fix: quest land keeps fixing a PR while each round makes progress, and
hands it back when the failure a fix addressed recurs right after it.

You have no GitHub credential and must not seek one. To tell a PR's maintainer
something, such as what a fix changed and why, a flaky test, or the decision a
handback needs, add an optional `"comment"` field to that PR's entry: Markdown of
Expand Down
4 changes: 2 additions & 2 deletions quest/m0/host-land/fixer.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ back for them.
- ✅ Keep as decided

The quest CLI implements `quest land fix`, durable collection, validation,
lease-pushes, Review records, and one attempt per maintainer pick (the
watcher's [stop rules](/quest/m0/queue-watcher/stop.md) replace that limit). Host work
lease-pushes, Review records, and the stop rules that end a pick's fixer
rounds (`stopRule` in `src/cli/land-fixer.ts`). Host work
remains in kixelated/nixos: the ephemeral fixer container (`qland-fix`) reaches
CLIProxyAPI and, like the check containers, the public internet without private
ranges (`modules/ci-egress.nix`), so its builds fetch dependencies;
Expand Down
12 changes: 7 additions & 5 deletions quest/m0/land-fixer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ moq-dev/rust2, moq-dev/rind and moq-dev/quest.
| 16 | fixer disappeared before recording an exit status | fixed on the host; none after 2026-10-31 04:00Z |
| 7 | fixer repository exceeds its size cap | fixed by #332 |
| 6 | AGENTS.md "pull the latest origin changes" read as an injection | fixed by [#325](https://github.com/moq-dev/quest/pull/325) and [#328](https://github.com/moq-dev/quest/pull/328); none after 2026-10-31 18:46Z |
| 98 | "already used its mechanical fix attempt" after a collateral requeue or an earlier conflict fix; each needed a manual re-pick | [Attempt budget](/quest/m0/queue-watcher/stop.md), batch triage ([moq-dev/quest#355](https://github.com/moq-dev/quest/pull/355)) |
| 98 | "already used its mechanical fix attempt" after a collateral requeue or an earlier conflict fix; each needed a manual re-pick | stop rules ([moq-dev/quest#384](https://github.com/moq-dev/quest/pull/384)), batch triage ([moq-dev/quest#355](https://github.com/moq-dev/quest/pull/355)) |
| ~75 | "requeued unchanged": collateral of a batch another PR broke | batch triage ([moq-dev/quest#355](https://github.com/moq-dev/quest/pull/355)) |
| 36 | picked again while fixing; the newer pick is kept, so nothing is lost | not planned |
| 17 | conflicts in protected paths (`flake.nix`, `.github/`), mostly mechanical; resolved by hand on moq-dev/moq#7344, #7315, #7318 | blocked by design; fixer-written protected edits become a ready commit ([Protected edits as ready commits](/quest/m0/land-fixer/protected-commit.md)) |
Expand All @@ -53,7 +53,8 @@ Decision prompts, selected options marked ✅:
- ✅ Broader: also fold batch triage ([moq-dev/quest#355](https://github.com/moq-dev/quest/pull/355))
and [Local mergeability](/quest/m0/queue-watcher/bad-picks.md) into
this epic
- How attempts count: see [Attempt budget](/quest/m0/queue-watcher/stop.md).
- How attempts count: unlimited while progressing, with three stop rules
([moq-dev/quest#384](https://github.com/moq-dev/quest/pull/384)).
- How the fixer gets dependencies:
- Host prefetches the job's lockfile dependencies and mounts them read-only
- ✅ Allow registry network. The host already gave the fixer container
Expand Down Expand Up @@ -116,8 +117,8 @@ Decision prompts, selected options marked ✅:
the daemon's markers, `quest status`) · keep the token
- Flaky tests: ✅ disable only a rerun-proven flaky test, in a separate PR
· keep the fixer's judgment and the base PR
- New work, all picked: ship the attempt budget (now
[Stop rules](/quest/m0/queue-watcher/stop.md)) right after triage; the
- New work, all picked: ship the attempt budget (now the stop rules,
[moq-dev/quest#384](https://github.com/moq-dev/quest/pull/384)) right after triage; the
batch check ([moq-dev/quest#379](https://github.com/moq-dev/quest/pull/379));
pre-checks ([#377](https://github.com/moq-dev/quest/pull/377)); and
tuning ([#376](https://github.com/moq-dev/quest/pull/376)). All three were
Expand All @@ -132,7 +133,8 @@ Decision prompts, selected options marked ✅:
through GitHub's merge queue one PR at a time, and a host watcher runs this
fixer on each PR the queue drops and re-enqueues it. Batching is gone, so
this epic keeps only the fixer's own quality work. Moved to the watcher:
the attempt budget (now [Stop rules](/quest/m0/queue-watcher/stop.md)),
the attempt budget (now the stop rules,
[moq-dev/quest#384](https://github.com/moq-dev/quest/pull/384)),
local mergeability and CI-failure parking (now
[Bad picks](/quest/m0/queue-watcher/bad-picks.md)), and flaky tests (now
[Flaky failures](/quest/m0/queue-watcher/flaky.md), without the rerun).
Expand Down
11 changes: 6 additions & 5 deletions quest/m0/queue-watcher/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,12 @@ Decision prompts, selected options marked ✅:
passed
- The front
- Attempts:
- ✅ Unlimited while progressing; see
[Stop rules](/quest/m0/queue-watcher/stop.md). This replaces batch
triage's ([moq-dev/quest#355](https://github.com/moq-dev/quest/pull/355))
one attempt per pick.
- ✅ Unlimited while progressing, handing back on a round with no new
commit for a failure that isn't flaky, the failure a fix addressed
recurring right after it, or three flaky rounds in a row. This replaces
batch triage's ([moq-dev/quest#355](https://github.com/moq-dev/quest/pull/355))
one attempt per pick; the host gate applies the same rules
([moq-dev/quest#384](https://github.com/moq-dev/quest/pull/384)).
- Flaky failures:
- ✅ The fixer re-enqueues them unchanged, and a test seen flaking twice is
disabled in a separate PR with a follow-up quest; the rerun before blame
Expand Down Expand Up @@ -110,7 +112,6 @@ Shared shape, for the children to follow:
- [Picks enter the merge queue](/quest/m0/queue-watcher/pick.md) - `quest merge --land` enqueues the pinned head into GitHub's merge queue and records the pick where the watcher reads it
- [Queue removals](/quest/m0/queue-watcher/removals.md) - the watcher sees each PR the queue drops, why, and the failing build's log
- [Fix and re-enqueue](/quest/m0/queue-watcher/fix.md) - the fixer runs on each dropped PR in the background, and the watcher re-enqueues it at the back
- [Stop rules](/quest/m0/queue-watcher/stop.md) - the fixer keeps going while each round changes something, and hands back on a no-op fix, a repeated failure, or three flaky re-enqueues
- [Bad picks](/quest/m0/queue-watcher/bad-picks.md) - a pick GitHub refuses to enqueue is merged locally with the quest driver or fixed, then pushed and enqueued
- [Flaky failures](/quest/m0/queue-watcher/flaky.md) - a flaky failure is re-enqueued unchanged, and a test seen flaking twice is disabled in a separate PR with a follow-up quest
- [Watcher on the host](/quest/m0/queue-watcher/host.md) - kixelated/nixos runs the watcher for moq-dev/moq beside its fixer container
Expand Down
2 changes: 1 addition & 1 deletion quest/m0/queue-watcher/bad-picks.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ since on a merge-queue base such a PR is a pick GitHub won't enqueue.
push. A clean driver merge needs no review: it is a conflict-free base merge.
- Fetch failing required checks' logs through the App's `checks: read` and
`actions: read`, trimmed like a queue failure's.
- The same [stop rules](/quest/m0/queue-watcher/stop.md) apply.
- The same stop rules apply (`stopRule` in `src/cli/land-fixer.ts`).

## Required

Expand Down
13 changes: 11 additions & 2 deletions quest/m0/queue-watcher/fix.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,17 @@ going while each round makes progress.
queue's builds in `quest gate`, and pass an explicit
`model_reasoning_effort` (default `medium`, with a daemon option), since
fixer logs show `reasoning effort: none`.
- When to stop is [Stop rules](/quest/m0/queue-watcher/stop.md); flaky
handling is [Flaky failures](/quest/m0/queue-watcher/flaky.md).
- Apply the stop rules the host gate already applies
([moq-dev/quest#384](https://github.com/moq-dev/quest/pull/384)): record
each round in the pick's history with `saveRound` and ask `stopRule`
(`src/cli/land-fixer.ts`) whether it hands back, as `collectJob` does. A
round is `fixed` for a fixer commit, `merged` for a clean quest-driver merge
of a conflict, `flaky` for the fixer's flaky verdict, and `unchanged` for
any other round with no new commit. Its failure signature is the test or
step the fixer names in its result's `failure`, falling back to the failing
job and step of the `merge_group` run; a conflict's is `conflictFailure`
of the base it conflicted with. A rule's handback posts `stopReason`.
Flaky handling is [Flaky failures](/quest/m0/queue-watcher/flaky.md).

## Required

Expand Down
5 changes: 3 additions & 2 deletions quest/m0/queue-watcher/flaky.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ apply (2026-11-01, user and maintainer):
What counts: the second flaky verdict for the same test, across any PRs, in
the watcher's state. A single verdict only re-enqueues, since disabling on one
judgment alone risks hiding a real bug. Each verdict names the test in the
same signature [Stop rules](/quest/m0/queue-watcher/stop.md) uses, and three
flaky re-enqueues of one PR in a row hand it back there.
same signature the stop rules use (the fixer result's `failure`), and the
third flaky round of one PR in a row hands it back there (`stopRule` in
`src/cli/land-fixer.ts`).

The fixer container holds no GitHub credentials, so the fixer returns the
disabling commit in its result and the watcher pushes it to a new branch and
Expand Down
41 changes: 0 additions & 41 deletions quest/m0/queue-watcher/stop.md

This file was deleted.

17 changes: 15 additions & 2 deletions src/cli/land-fix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ export function readFixResult(root: string): string {
}
/** The longest Markdown comment a result may ask the daemon to post on its PR. */
export const COMMENT_LIMIT = 4000;
/** The longest failure signature a result may name, short enough to quote in a handback. */
const FAILURE_LIMIT = 200;

/**
* The fixer's verdict on one PR. `blame` names a PR whose own change caused the failure; a blamed PR
* is fixed or handed back, never requeued. A handback's `log` cites the failure log's lines, first
* to last, that show the failure, which its comment quotes.
* to last, that show the failure, which its comment quotes. `failure` names the failing test or step,
* the signature the stop rules compare across a pick's rounds.
*/
export type FixResult = { agent?: string; model?: string; blame?: boolean; comment?: string } & (
export type FixResult = { agent?: string; model?: string; blame?: boolean; comment?: string; failure?: string } & (
| { number: number; action: "requeue"; reason: string }
| { number: number; action: "fixed"; head: string; reason: string }
| { number: number; action: "handback"; diagnosis: string; recommendation: string; log?: [number, number] }
Expand Down Expand Up @@ -86,6 +89,16 @@ function validate(item: any, pr: boolean): void {
throw new Error("invalid fixer log lines");
metadata.push("log");
}
if (pr && "failure" in item) {
if (
typeof item.failure !== "string" ||
!item.failure.trim() ||
item.failure.length > FAILURE_LIMIT ||
/[\u0000-\u001f\u007f]/.test(item.failure)
)
throw new Error("invalid fixer failure");
metadata.push("failure");
}
// One optional Markdown comment per PR: multiline, but no other control characters.
if (pr && "comment" in item) {
if (
Expand Down
Loading
Loading