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
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ Neither command touches branches or PRs.

`quest wait` blocks until a GitHub PR has a review, comment, review-thread
reply, reaction to the PR or one of its reviews, push, merge, close, removal
from the merge queue, or disabled auto-merge:
from the merge queue, disabled auto-merge, or required CI result:

```sh
quest wait 101
Expand Down Expand Up @@ -242,8 +242,24 @@ timeline, excluding the one that created the starting head. A merge through
the queue prints `removed_from_merge_queue` about a second before `merged`, so a
poll can catch the removal alone; check `quest status` before treating it as a
failed merge. Actors are printed without deciding
whose review counts. Check runs and edits to existing comments or reviews are
outside this command's scope.
whose review counts. Edits to existing comments or reviews are outside this
command's scope.

CI results are read for the current head. The first required check to fail
prints `checks:failed` right away. `checks:passed` prints once nothing on the
head is still going (no pending check and no unfinished GitHub Actions run, which
may still create jobs), every check and workflow the base branch names is
present, and every required check passed; it names the last check to finish,
when the head went idle. Each prints once per head, with the check's name
(spaces as `_`) in the actor field and its details URL. Results count as
`quest status` counts them, so neutral and skipped results pass. Each job of a
workflow a ruleset requires is required.
Requiredness comes from the base branch's protection and rulesets, readable
with read access; optional checks never wake the wait, unless the base branch
requires none, in which case every check is required. A result counts when it
was reached at or after the starting point, so a wait started after CI finished
still reports it; a push starts over for the new head, and the cursor records
what was reported, so resuming never replays it.

Review-thread replies and reactions never reach the PR timeline, so they are
read separately. A reply prints as `replied`; a thread's first comment is part
Expand All @@ -259,8 +275,9 @@ visible after a newer reaction is still reported.

Each poll makes one conditional timeline request and one conditional
review-comment request every five minutes once it has read the existing pages,
following a new page when the current one fills, plus one GraphQL query for
reactions. It honors GitHub's rate-limit headers and fails loudly on invalid responses.
following a new page when the current one fills, plus one GraphQL query each
for reactions and checks. It reads the base branch's required checks once per
call. It honors GitHub's rate-limit headers and fails loudly on invalid responses.
There is no deadline unless `--timeout <seconds>` is supplied. Activity exits 0,
timeout exits 3, invalid arguments exit 2, and other failures exit 1; interrupt
with Ctrl-C to stop waiting. A timeout prints a resume point as its only line:
Expand Down
2 changes: 1 addition & 1 deletion assets/skills/complete.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Consider ordering, queuing any merges if they would result in conflicts.

Run each `/quest-ready`, `/quest-merge`, or `/quest-iterate` in its own sub-agent with its own isolated worktree.
Each sub-agent blocks on its own waits and reports back only when done or blocked.
For GitHub review or merge activity, it uses `quest wait` and resumes with the
For GitHub CI, review, or merge activity, it uses one `quest wait` and resumes with the
returned cursor as described by `quest skill merge`, rather than polling GitHub.
Limit the number of active agents to the CPU core count.
Other sessions share this machine: hold new agents while the load average exceeds the core count.
Expand Down
9 changes: 5 additions & 4 deletions assets/skills/merge.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,17 @@ This includes deciding if we should `/quest-plan` for suggested follow-ups.

A background agent that cannot prompt lists its decisions and suggested follow-ups in its report instead.

When waiting for a required review or for an enabled auto-merge to finish, run
When waiting for CI, a required review, or an enabled auto-merge to finish, run
`quest wait <pr> --timeout 3300` as one blocking command instead of polling
GitHub in a loop. Inspect the events and decide whether they satisfy the wait;
GitHub in a loop; it also reports the head's required checks as `checks:failed`
or `checks:passed`, so no separate check watcher is needed.
Inspect the events and decide whether they satisfy the wait;
the command does not decide whose review counts. If activity is irrelevant
(including bot notices), call it again with `--since` set to the last field of
its last line. Exit 3 means the deadline passed with no activity; re-run with
`--since` set to the printed cursor, so nothing during the first wait is missed.
Other failures must be reported rather than treated as activity.
Check CI with its own blocking check watcher, bounded under an hour the same
way; check runs are not PR timeline activity. Do not wait for optional reviews.
Do not wait for optional reviews.
Run each wait so its exit wakes you, such as a background task that notifies on
exit, and end your turn until then. Never poll its output or rerun it under a
foreground time limit; each wake rereads your whole context. The hour bound
Expand Down
1 change: 0 additions & 1 deletion quest/m0/helpers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ commands from Sep-Oct. Session counts in each child come from that window.

## Required

- [CI checks in wait](/quest/m0/helpers/wait-checks.md) - `quest wait` wakes on required check results for the head
- [PR state commands](/quest/m0/helpers/pr-states.md) - `quest iterate`, `ready`, `merge`, and `release` label the one owner of a PR
- [Headless agents](/quest/m0/helpers/headless-agents.md) - `/quest-spawn api` launches, resumes, and stops billed agents through one command auto mode allows

Expand Down
31 changes: 0 additions & 31 deletions quest/m0/helpers/wait-checks.md

This file was deleted.

9 changes: 7 additions & 2 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,14 +253,19 @@ export function main(argv: string[], out: Output): number | Promise<number> {
.summary("Wait for pull request activity on GitHub")
.description(
"Wait for a review, comment, review-thread reply, reaction to the PR or a review, push, merge, close, " +
"merge-queue removal, or disabled auto-merge. Polls every 5 minutes; rate limits delay requests. No timeout by default.\n\n" +
"merge-queue removal, disabled auto-merge, or required CI result. Polls every 5 minutes; rate limits delay requests. No timeout by default.\n\n" +
"Prints one plain line per event: timestamp (or commit SHA), type, actor, URL; the last line adds a resume cursor. " +
"Reactions print as reacted:<content>, such as reacted:thumbs_up. " +
"The head's first failing required check prints as checks:failed, and every required check passing as checks:passed, " +
"each once per head, with the check's name as the actor and its details URL. " +
"Results count as quest status counts them, so neutral and skipped results pass. " +
"checks:passed waits until no check is pending, no GitHub Actions run is unfinished, and every check and workflow the base branch names is present, then names the last check to finish. " +
"Optional checks never wake the wait, unless the base branch requires none, which requires them all. " +
"Pass the final field of the last line to --since to resume without replaying events. " +
"On timeout, the only line is a resume point for --since (a cursor, or the given --since or head SHA if setup did not finish), so a retry with it still reports activity from the first wait.\n\n" +
"Uses GH_TOKEN, GITHUB_TOKEN, or gh auth token. Replies and reactions by that account are ignored. " +
"Exits 0 for activity, 3 on timeout, 2 for invalid arguments, and 1 for other failures. " +
"Check runs and edits are not activity.",
"Edits are not activity.",
)
.argument("<pr>", "GitHub PR URL, or a PR number in origin")
.option("--since <point>", "ISO timestamp, pushed-tip SHA, or resume cursor (default: current head push time)")
Expand Down
40 changes: 26 additions & 14 deletions src/cli/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,20 +178,9 @@ function summarize(
pending: [],
} as Status["checks"];
for (const context of contexts) {
const run = context.__typename === "CheckRun";
const check: Check = {
name: string(run ? context.name : context.context),
required: context.isRequired === true,
result:
run && context.status !== "COMPLETED"
? lower(context.status)
: lower(run ? context.conclusion : context.state),
url: optional(run ? context.detailsUrl : context.targetUrl),
};
if (run ? context.status !== "COMPLETED" : ["pending", "expected"].includes(check.result))
checks.pending.push(check);
else if (run ? PASSING.includes(check.result) : check.result === "success") checks.passed++;
else checks.failing.push(check);
const { check, outcome } = classifyCheck(context);
if (outcome === "passed") checks.passed++;
else checks[outcome].push(check);
}

const reactions: Reaction[] = [];
Expand Down Expand Up @@ -267,6 +256,29 @@ function summarize(
};
}

/**
* A check run or commit status from a `statusCheckRollup` context, and whether it passed, failed, or
* is still pending. `quest wait` shares this, so both commands agree on what passes.
*/
export function classifyCheck(context: ObjectValue): { check: Check; outcome: "passed" | "failing" | "pending" } {
const run = context.__typename === "CheckRun";
const check: Check = {
name: string(run ? context.name : context.context),
required: context.isRequired === true,
result:
run && context.status !== "COMPLETED"
? lower(context.status)
: lower(run ? context.conclusion : context.state),
url: optional(run ? context.detailsUrl : context.targetUrl),
};
if (run ? context.status !== "COMPLETED" : ["pending", "expected"].includes(check.result))
return { check, outcome: "pending" };
return {
check,
outcome: (run ? PASSING.includes(check.result) : check.result === "success") ? "passed" : "failing",
};
}

/** Stable plain lines: a record type, then space-separated fields, with any free text last. */
export function statusLines(status: Status): string[] {
const dash = (value: string | number | null) => (value === null ? "-" : String(value));
Expand Down
Loading
Loading