@quickchatai/quickreview 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -19
- package/config.example.toml +41 -9
- package/dist/extensions/cli/login-route.js +5 -0
- package/dist/extensions/cli/login-route.js.map +1 -0
- package/dist/extensions/login-route.js +180 -0
- package/dist/extensions/login-route.js.map +1 -0
- package/dist/extensions/onboarding.js +29 -6
- package/dist/extensions/onboarding.js.map +1 -1
- package/dist/src/ciretry.js +2 -1
- package/dist/src/ciretry.js.map +1 -1
- package/dist/src/cli.js +43 -12
- package/dist/src/cli.js.map +1 -1
- package/dist/src/cliops.js +3 -2
- package/dist/src/cliops.js.map +1 -1
- package/dist/src/commands.js +25 -2
- package/dist/src/commands.js.map +1 -1
- package/dist/src/config.js +16 -2
- package/dist/src/config.js.map +1 -1
- package/dist/src/daemon.js +24 -8
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/debugroundpi.js +5 -1
- package/dist/src/debugroundpi.js.map +1 -1
- package/dist/src/dispatch.js +5 -4
- package/dist/src/dispatch.js.map +1 -1
- package/dist/src/engine/debugpi.js +17 -5
- package/dist/src/engine/debugpi.js.map +1 -1
- package/dist/src/execboundary.js +10 -3
- package/dist/src/execboundary.js.map +1 -1
- package/dist/src/github.js +51 -16
- package/dist/src/github.js.map +1 -1
- package/dist/src/githubtoken.js +346 -0
- package/dist/src/githubtoken.js.map +1 -0
- package/dist/src/loginhint.js +8 -3
- package/dist/src/loginhint.js.map +1 -1
- package/dist/src/loginrefresh/pirefresh.js +3 -0
- package/dist/src/loginrefresh/pirefresh.js.map +1 -1
- package/dist/src/npmverbs.js +4 -2
- package/dist/src/npmverbs.js.map +1 -1
- package/dist/src/onboard/choices.js +29 -1
- package/dist/src/onboard/choices.js.map +1 -1
- package/dist/src/onboard/engine.js +8 -23
- package/dist/src/onboard/engine.js.map +1 -1
- package/dist/src/onboard/launch.js +12 -20
- package/dist/src/onboard/launch.js.map +1 -1
- package/dist/src/onboard/pipin.js +22 -0
- package/dist/src/onboard/pipin.js.map +1 -0
- package/dist/src/onboard/stepforward.js +41 -0
- package/dist/src/onboard/stepforward.js.map +1 -0
- package/dist/src/onboard/steps/appslug.js +4 -1
- package/dist/src/onboard/steps/appslug.js.map +1 -1
- package/dist/src/onboard/steps/compose.js +29 -0
- package/dist/src/onboard/steps/compose.js.map +1 -1
- package/dist/src/onboard/steps/configfill.js +102 -32
- package/dist/src/onboard/steps/configfill.js.map +1 -1
- package/dist/src/onboard/steps/githubaccount.js +595 -0
- package/dist/src/onboard/steps/githubaccount.js.map +1 -0
- package/dist/src/onboard/steps/githubapp.js +65 -7
- package/dist/src/onboard/steps/githubapp.js.map +1 -1
- package/dist/src/onboard/steps/golive.js +2 -0
- package/dist/src/onboard/steps/golive.js.map +1 -1
- package/dist/src/onboard/steps/index.js +11 -0
- package/dist/src/onboard/steps/index.js.map +1 -1
- package/dist/src/onboard/steps/ingress.js +80 -25
- package/dist/src/onboard/steps/ingress.js.map +1 -1
- package/dist/src/onboard/steps/llmauth.js +103 -30
- package/dist/src/onboard/steps/llmauth.js.map +1 -1
- package/dist/src/onboard/steps/memory.js +53 -1
- package/dist/src/onboard/steps/memory.js.map +1 -1
- package/dist/src/onboard/steps/proof.js +366 -56
- package/dist/src/onboard/steps/proof.js.map +1 -1
- package/dist/src/onboard/steps/topology.js +6 -6
- package/dist/src/onboard/steps/topology.js.map +1 -1
- package/dist/src/onboard/steps/transport.js +9 -1
- package/dist/src/onboard/steps/transport.js.map +1 -1
- package/dist/src/onboard/steps/tuiroute.js +102 -0
- package/dist/src/onboard/steps/tuiroute.js.map +1 -0
- package/dist/src/onboard/steps/verifyup.js +17 -1
- package/dist/src/onboard/steps/verifyup.js.map +1 -1
- package/dist/src/onboard/toolnames.js +30 -0
- package/dist/src/onboard/toolnames.js.map +1 -0
- package/dist/src/operatorwrite.js +23 -0
- package/dist/src/operatorwrite.js.map +1 -0
- package/dist/src/outcomes.js +4 -1
- package/dist/src/outcomes.js.map +1 -1
- package/dist/src/posting-journal.js +2 -1
- package/dist/src/posting-journal.js.map +1 -1
- package/dist/src/posting-outbox.js +6 -2
- package/dist/src/posting-outbox.js.map +1 -1
- package/dist/src/posting-receipt.js +14 -1
- package/dist/src/posting-receipt.js.map +1 -1
- package/dist/src/recheck.js +2 -1
- package/dist/src/recheck.js.map +1 -1
- package/dist/src/reconcile.js +22 -2
- package/dist/src/reconcile.js.map +1 -1
- package/dist/src/reviewhistory.js +32 -6
- package/dist/src/reviewhistory.js.map +1 -1
- package/dist/src/reviewnow.js +45 -0
- package/dist/src/reviewnow.js.map +1 -0
- package/dist/src/state.js.map +1 -1
- package/dist/src/statemachine.js +4 -1
- package/dist/src/statemachine.js.map +1 -1
- package/dist/src/takeoverpi.js +13 -0
- package/dist/src/takeoverpi.js.map +1 -1
- package/dist/src/tui/app.js +49 -0
- package/dist/src/tui/app.js.map +1 -1
- package/dist/src/tui/board.js +88 -12
- package/dist/src/tui/board.js.map +1 -1
- package/dist/src/tui/cmdbar.js +4 -1
- package/dist/src/tui/cmdbar.js.map +1 -1
- package/dist/src/tui/companion.js +74 -10
- package/dist/src/tui/companion.js.map +1 -1
- package/dist/src/tui/connect.js +7 -1
- package/dist/src/tui/connect.js.map +1 -1
- package/dist/src/tui/filters.js +5 -2
- package/dist/src/tui/filters.js.map +1 -1
- package/dist/src/tui/ghread.js +61 -5
- package/dist/src/tui/ghread.js.map +1 -1
- package/dist/src/tui/index.js +106 -20
- package/dist/src/tui/index.js.map +1 -1
- package/dist/src/tui/memforward.js +39 -2
- package/dist/src/tui/memforward.js.map +1 -1
- package/dist/src/tui/model.js +25 -0
- package/dist/src/tui/model.js.map +1 -1
- package/dist/src/tui/prpause.js +7 -0
- package/dist/src/tui/prpause.js.map +1 -1
- package/dist/src/tui/remote.js +7 -1
- package/dist/src/tui/remote.js.map +1 -1
- package/dist/src/tui/route.js +143 -0
- package/dist/src/tui/route.js.map +1 -0
- package/dist/src/tui/setupstate.js +29 -0
- package/dist/src/tui/setupstate.js.map +1 -0
- package/dist/src/tui/sshtriage.js +102 -5
- package/dist/src/tui/sshtriage.js.map +1 -1
- package/dist/src/tui/theme.js +2 -1
- package/dist/src/tui/theme.js.map +1 -1
- package/dist/src/tui/unwedge.js +2 -12
- package/dist/src/tui/unwedge.js.map +1 -1
- package/dist/src/verdicts.js +17 -5
- package/dist/src/verdicts.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/src/webhook.js +12 -1
- package/dist/src/webhook.js.map +1 -1
- package/dist/src/wire.js +28 -6
- package/dist/src/wire.js.map +1 -1
- package/dist/src/workerworld.js +7 -1
- package/dist/src/workerworld.js.map +1 -1
- package/docker/companion/entrypoint.sh +20 -1
- package/docker/entrypoint.sh +17 -0
- package/extensions/cli/login-route.ts +10 -0
- package/extensions/login-route.ts +220 -0
- package/extensions/onboarding.ts +35 -6
- package/package.json +2 -2
- package/prompts/onboarding.md +57 -18
- package/scripts/mint-token.sh +25 -4
- package/src/ciretry.ts +2 -1
- package/src/cli.ts +49 -14
- package/src/cliops.ts +3 -2
- package/src/commands.ts +28 -2
- package/src/config.ts +24 -4
- package/src/daemon.ts +32 -7
- package/src/debugroundpi.ts +11 -1
- package/src/dispatch.ts +5 -4
- package/src/engine/debugpi.ts +17 -5
- package/src/execboundary.ts +16 -4
- package/src/github.ts +57 -15
- package/src/githubtoken.ts +435 -0
- package/src/loginhint.ts +8 -3
- package/src/loginrefresh/pirefresh.ts +3 -0
- package/src/npmverbs.ts +4 -2
- package/src/onboard/choices.ts +34 -1
- package/src/onboard/engine.ts +22 -3
- package/src/onboard/launch.ts +17 -29
- package/src/onboard/pipin.ts +31 -0
- package/src/onboard/stepforward.ts +61 -0
- package/src/onboard/steps/appslug.ts +4 -1
- package/src/onboard/steps/compose.ts +31 -0
- package/src/onboard/steps/configfill.ts +121 -33
- package/src/onboard/steps/githubaccount.ts +716 -0
- package/src/onboard/steps/githubapp.ts +92 -8
- package/src/onboard/steps/golive.ts +2 -0
- package/src/onboard/steps/index.ts +11 -0
- package/src/onboard/steps/ingress.ts +98 -31
- package/src/onboard/steps/llmauth.ts +114 -29
- package/src/onboard/steps/memory.ts +58 -1
- package/src/onboard/steps/proof.ts +473 -55
- package/src/onboard/steps/topology.ts +7 -6
- package/src/onboard/steps/transport.ts +15 -0
- package/src/onboard/steps/tuiroute.ts +119 -0
- package/src/onboard/steps/verifyup.ts +20 -1
- package/src/onboard/toolnames.ts +34 -0
- package/src/operatorwrite.ts +25 -0
- package/src/outcomes.ts +8 -1
- package/src/posting-journal.ts +6 -1
- package/src/posting-outbox.ts +10 -2
- package/src/posting-receipt.ts +17 -0
- package/src/recheck.ts +2 -1
- package/src/reconcile.ts +29 -3
- package/src/reviewhistory.ts +20 -6
- package/src/reviewnow.ts +76 -0
- package/src/state.ts +18 -0
- package/src/statemachine.ts +4 -1
- package/src/takeoverpi.ts +13 -0
- package/src/tui/app.ts +47 -0
- package/src/tui/board.ts +111 -14
- package/src/tui/cmdbar.ts +4 -1
- package/src/tui/companion.ts +78 -10
- package/src/tui/connect.ts +7 -1
- package/src/tui/filters.ts +7 -3
- package/src/tui/ghread.ts +74 -7
- package/src/tui/index.ts +114 -23
- package/src/tui/memforward.ts +52 -3
- package/src/tui/model.ts +36 -2
- package/src/tui/prpause.ts +11 -2
- package/src/tui/remote.ts +9 -1
- package/src/tui/route.ts +173 -0
- package/src/tui/setupstate.ts +52 -0
- package/src/tui/sshtriage.ts +127 -6
- package/src/tui/theme.ts +2 -1
- package/src/tui/unwedge.ts +2 -14
- package/src/verdicts.ts +23 -5
- package/src/version.ts +1 -1
- package/src/webhook.ts +10 -1
- package/src/wire.ts +36 -7
- package/src/workerworld.ts +7 -1
|
@@ -4,14 +4,23 @@
|
|
|
4
4
|
// the review it produced before anything could ever reach GitHub.
|
|
5
5
|
//
|
|
6
6
|
// The shape:
|
|
7
|
-
// 1. ask for
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// 1. ask for one real, open PR (a number or its URL; the five most recently
|
|
8
|
+
// updated open PRs of the repo are offered, the person's own included —
|
|
9
|
+
// on the own-account route those are the main target). The wizard never
|
|
10
|
+
// creates a demo PR;
|
|
11
|
+
// 2. start its review IMMEDIATELY: one `review-now` command into the
|
|
12
|
+
// daemon's root-sealed operator channel (src/commands.ts, the channel
|
|
13
|
+
// `qcr drain` and the TUI's levers use), which queues the PR exactly as a
|
|
14
|
+
// fresh trigger label would — with no GitHub write and no label for the
|
|
15
|
+
// person to add. A daemon that cannot take the command (an older image)
|
|
16
|
+
// falls back to the label, said plainly;
|
|
17
|
+
// 3. follow the round to completion THROUGH THE STATE STORE — the daemon's
|
|
10
18
|
// own record, read with the daemon's own `qcr why --json` inside the
|
|
11
19
|
// container, so the wizard is reading exactly what the operator would;
|
|
12
|
-
//
|
|
13
|
-
// mutation gate wrote INSTEAD of calling GitHub
|
|
14
|
-
//
|
|
20
|
+
// 4. SHOW the finished review, read out of the dry-run record — the file the
|
|
21
|
+
// mutation gate wrote INSTEAD of calling GitHub: the verdict, how many
|
|
22
|
+
// findings, the top ones, and where the full text is. The operator judges
|
|
23
|
+
// the output while it is still impossible for it to have posted.
|
|
15
24
|
//
|
|
16
25
|
// Two paths get real guidance rather than a spinner, because they are the two
|
|
17
26
|
// that actually happen: the round never starts (label spelling, webhook not
|
|
@@ -19,11 +28,16 @@
|
|
|
19
28
|
// is cancelled (the daemon's own event log says why). Neither ever hangs: the
|
|
20
29
|
// poll is bounded and every attempt logs where it got to.
|
|
21
30
|
//
|
|
22
|
-
//
|
|
23
|
-
// dry-run directory
|
|
24
|
-
//
|
|
31
|
+
// The one thing it writes is that operator command file. Otherwise it reads the
|
|
32
|
+
// state store, the dry-run directory and (to offer PRs) GitHub, and records
|
|
33
|
+
// what it saw on the checklist — the record the go-live step later demands
|
|
34
|
+
// before it will even offer the flip.
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
import { mintAppJwt, USER_TOKEN_FILE } from "@quickchatai/pi-quickreview/src/githubAuth.js";
|
|
25
37
|
import { loadConfig } from "../../config.js";
|
|
26
|
-
import {
|
|
38
|
+
import { operatorCommandWriteScriptAt } from "../../operatorwrite.js";
|
|
39
|
+
import { CHOICE_KEYS, userAuthRecorded } from "../choices.js";
|
|
40
|
+
import { readTextOrNull } from "../engine.js";
|
|
27
41
|
import type {
|
|
28
42
|
CheckResult,
|
|
29
43
|
OnboardAdapters,
|
|
@@ -37,12 +51,30 @@ import {
|
|
|
37
51
|
type DryrunRecord,
|
|
38
52
|
type ReviewSnapshot,
|
|
39
53
|
} from "../../compare.js";
|
|
40
|
-
import {
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
import {
|
|
55
|
+
CONTAINER_COMMANDS_DIR,
|
|
56
|
+
CONTAINER_DRYRUN_DIR,
|
|
57
|
+
configPath,
|
|
58
|
+
readConfigText,
|
|
59
|
+
} from "./compose.js";
|
|
60
|
+
import { readTomlKey, secretsDirFor } from "./ingress.js";
|
|
61
|
+
import {
|
|
62
|
+
DEFAULT_LABELS,
|
|
63
|
+
api,
|
|
64
|
+
findInstallation,
|
|
65
|
+
installationToken,
|
|
66
|
+
parseRepoSlug,
|
|
67
|
+
readCredentials,
|
|
68
|
+
type RepoSlug,
|
|
69
|
+
} from "./githubapp.js";
|
|
43
70
|
import { stackTransportFor, type StackTransport, type TransportOptions } from "./transport.js";
|
|
44
71
|
|
|
45
72
|
export const PROOF_STEP_ID = "proof_round";
|
|
73
|
+
|
|
74
|
+
/** A string input or choice, else "" — never an object's stringification. */
|
|
75
|
+
function textOf(v: unknown): string {
|
|
76
|
+
return typeof v === "string" ? v : "";
|
|
77
|
+
}
|
|
46
78
|
/** The completed-proof record. The go-live step will not offer the flip
|
|
47
79
|
* without it AND a state-store re-verification of it. */
|
|
48
80
|
export const PROOF_CHOICE_KEY = CHOICE_KEYS.proof;
|
|
@@ -238,14 +270,58 @@ export function recordForPr(r: DryrunRecord, pr: number): boolean {
|
|
|
238
270
|
return r.pr === pr || new RegExp(`/(pulls|issues)/${pr}(/|$)`).test(r.path);
|
|
239
271
|
}
|
|
240
272
|
|
|
273
|
+
/** How many findings the summary lists before "and N more". */
|
|
274
|
+
export const TOP_FINDINGS = 5;
|
|
275
|
+
|
|
276
|
+
export interface ReviewRenderExtras {
|
|
277
|
+
/** The round's verdict on the merits (the review EVENT can be COMMENT on
|
|
278
|
+
* the own-account route, where the verdict is stated in the body). */
|
|
279
|
+
verdict?: string | null;
|
|
280
|
+
/** Where the full record is, and the command that prints it. */
|
|
281
|
+
file?: string;
|
|
282
|
+
readCommand?: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** The first line of a comment, trimmed to one readable line. */
|
|
286
|
+
function headline(body: string): string {
|
|
287
|
+
const line =
|
|
288
|
+
body
|
|
289
|
+
.trim()
|
|
290
|
+
.split("\n")
|
|
291
|
+
.find((l) => l.trim() !== "") ?? "";
|
|
292
|
+
const flat = line.trim();
|
|
293
|
+
return flat.length > 160 ? `${flat.slice(0, 157)}…` : flat;
|
|
294
|
+
}
|
|
295
|
+
|
|
241
296
|
/** The would-be review, rendered for a human to judge. */
|
|
242
|
-
export function renderReview(
|
|
297
|
+
export function renderReview(
|
|
298
|
+
review: ReviewSnapshot,
|
|
299
|
+
pr: number,
|
|
300
|
+
extras: ReviewRenderExtras = {},
|
|
301
|
+
): string {
|
|
302
|
+
const top = review.comments
|
|
303
|
+
.slice(0, TOP_FINDINGS)
|
|
304
|
+
.map((c, i) => ` ${i + 1}. ${c.path}:${c.line} — ${headline(c.body)}`);
|
|
305
|
+
const more = review.comments.length - top.length;
|
|
243
306
|
const head = [
|
|
244
307
|
`This is the review your reviewer produced for #${pr}. It was NOT posted: dry_run is on, so the`,
|
|
245
308
|
`mutation gate wrote it to state/dryrun/ instead of calling GitHub.`,
|
|
246
309
|
"",
|
|
247
310
|
`Verdict it would have submitted: ${review.state}`,
|
|
311
|
+
...(extras.verdict && extras.verdict !== review.state
|
|
312
|
+
? [
|
|
313
|
+
`Verdict on the merits: ${extras.verdict} (stated in the review body; GitHub gets it as a ${review.state} review)`,
|
|
314
|
+
]
|
|
315
|
+
: []),
|
|
248
316
|
`Inline comments: ${review.comments.length}`,
|
|
317
|
+
...(top.length > 0
|
|
318
|
+
? ["Top findings:", ...top, ...(more > 0 ? [` … and ${more} more, below`] : [])]
|
|
319
|
+
: []),
|
|
320
|
+
...(extras.file
|
|
321
|
+
? [
|
|
322
|
+
`Full text: state/dryrun/${extras.file} in the daemon's state${extras.readCommand ? ` — \`${extras.readCommand}\`` : ""}`,
|
|
323
|
+
]
|
|
324
|
+
: []),
|
|
249
325
|
"",
|
|
250
326
|
"--- review body ---",
|
|
251
327
|
review.body.trim() === "" ? "(empty body)" : review.body.trim(),
|
|
@@ -268,8 +344,18 @@ export interface ProofStepOptions {
|
|
|
268
344
|
* `stackTransportFor` decides it from the recorded topology, so the proof
|
|
269
345
|
* round is the same act on a laptop and on a server. */
|
|
270
346
|
transport?: TransportOptions;
|
|
347
|
+
/** GitHub's API base (the open-PR offer). */
|
|
348
|
+
apiBase?: string;
|
|
349
|
+
/** Secrets dir for the offer's read token; default: the one resolution. */
|
|
350
|
+
secretsDir?: string;
|
|
351
|
+
/** How many times the review-now result file is read before giving up. */
|
|
352
|
+
resultAttempts?: number;
|
|
353
|
+
resultIntervalMs?: number;
|
|
271
354
|
}
|
|
272
355
|
|
|
356
|
+
/** How many open PRs the offer lists. */
|
|
357
|
+
export const PR_OFFER_SIZE = 5;
|
|
358
|
+
|
|
273
359
|
function triggerLabelOf(adapters: OnboardAdapters): string {
|
|
274
360
|
const text = readConfigText(adapters);
|
|
275
361
|
const configured = text ? readTomlKey(text, "github", "trigger_label") : null;
|
|
@@ -283,22 +369,126 @@ function repoOf(choices: Readonly<Record<string, unknown>>, adapters: OnboardAda
|
|
|
283
369
|
return (text ? readTomlKey(text, "github", "repo") : null) ?? "your repository";
|
|
284
370
|
}
|
|
285
371
|
|
|
372
|
+
/** A PR answer: 17, "17", "#17", or the PR's URL (github.com/o/r/pull/17).
|
|
373
|
+
* A URL names its repo too, which the caller checks against the reviewed one. */
|
|
374
|
+
export function parsePrAnswer(raw: unknown): { pr: number; repo: string | null } | null {
|
|
375
|
+
if (typeof raw === "number")
|
|
376
|
+
return Number.isInteger(raw) && raw > 0 ? { pr: raw, repo: null } : null;
|
|
377
|
+
if (typeof raw !== "string") return null;
|
|
378
|
+
const v = raw.trim();
|
|
379
|
+
const url =
|
|
380
|
+
/^(?:https?:\/\/)?(?:www\.)?github\.com\/([A-Za-z0-9._-]+\/[A-Za-z0-9._-]+)\/pull\/(\d+)(?:[/?#].*)?$/.exec(
|
|
381
|
+
v,
|
|
382
|
+
);
|
|
383
|
+
if (url) return { pr: Number(url[2]), repo: url[1]! };
|
|
384
|
+
const n = /^#?(\d+)$/.exec(v);
|
|
385
|
+
return n && Number(n[1]) > 0 ? { pr: Number(n[1]), repo: null } : null;
|
|
386
|
+
}
|
|
387
|
+
|
|
286
388
|
function prFrom(
|
|
287
389
|
input: Readonly<Record<string, unknown>>,
|
|
288
390
|
choices: Readonly<Record<string, unknown>>,
|
|
289
|
-
): number | null {
|
|
290
|
-
for (const raw of [input.pr, parseProofRecord(choices[PROOF_CHOICE_KEY])?.pr]) {
|
|
291
|
-
const
|
|
292
|
-
if (
|
|
391
|
+
): { pr: number; repo: string | null } | null {
|
|
392
|
+
for (const raw of [input.pr, input.url, parseProofRecord(choices[PROOF_CHOICE_KEY])?.pr]) {
|
|
393
|
+
const parsed = parsePrAnswer(raw);
|
|
394
|
+
if (parsed) return parsed;
|
|
293
395
|
}
|
|
294
396
|
return null;
|
|
295
397
|
}
|
|
296
398
|
|
|
399
|
+
/** A command file name the daemon's watcher serves (`*.json`), unique per call. */
|
|
400
|
+
export function reviewNowFileName(now: Date, pr: number): string {
|
|
401
|
+
return `${now.toISOString().replace(/[:.]/g, "-")}-onboard-reviewnow-${pr}.json`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** The argv that writes one `review-now` command into the root-sealed
|
|
405
|
+
* operator directory, through the same script the TUI's levers use. The
|
|
406
|
+
* payload is built here from a number, so it can carry no quote. */
|
|
407
|
+
export function reviewNowWriteArgv(name: string, pr: number): string[] {
|
|
408
|
+
if (!Number.isInteger(pr) || pr <= 0) throw new Error(`not a PR number: ${pr}`);
|
|
409
|
+
const payload = JSON.stringify({ command: "review-now", pr });
|
|
410
|
+
return [
|
|
411
|
+
"sh",
|
|
412
|
+
"-c",
|
|
413
|
+
`printf '%s\\n' '${payload}' | ( ${operatorCommandWriteScriptAt(CONTAINER_COMMANDS_DIR, name)} )`,
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** The argv that prints the daemon's result for that command, or `pending`. */
|
|
418
|
+
export function reviewNowResultArgv(name: string): string[] {
|
|
419
|
+
const file = `${CONTAINER_COMMANDS_DIR}/results/${name.replace(/\.json$/, "")}.result.json`;
|
|
420
|
+
return ["sh", "-c", `if [ -f '${file}' ]; then cat '${file}'; else echo pending; fi`];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export interface OpenPr {
|
|
424
|
+
number: number;
|
|
425
|
+
title: string;
|
|
426
|
+
author: string;
|
|
427
|
+
updated_at: string;
|
|
428
|
+
draft: boolean;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** A token that can read the reviewed repo: the person's token file on the
|
|
432
|
+
* own-account route, an App installation token on the App route; null when
|
|
433
|
+
* neither is to hand (the offer is then skipped, never guessed). */
|
|
434
|
+
async function readToken(
|
|
435
|
+
ctx: StepContext,
|
|
436
|
+
repo: RepoSlug,
|
|
437
|
+
apiBase: string,
|
|
438
|
+
secretsDir: string,
|
|
439
|
+
): Promise<string | null> {
|
|
440
|
+
if (userAuthRecorded(ctx.choices)) {
|
|
441
|
+
const token = (readTextOrNull(join(secretsDir, USER_TOKEN_FILE)) ?? "").trim();
|
|
442
|
+
return token === "" ? null : token;
|
|
443
|
+
}
|
|
444
|
+
const creds = readCredentials(secretsDir);
|
|
445
|
+
if (!creds.complete) return null;
|
|
446
|
+
try {
|
|
447
|
+
const jwt = mintAppJwt(creds.appId!, creds.pem!, () => ctx.adapters.clock().getTime());
|
|
448
|
+
const found = await findInstallation(ctx.adapters.fetch, apiBase, jwt, repo.owner);
|
|
449
|
+
if (found.status !== "found") return null;
|
|
450
|
+
const minted = await installationToken(ctx.adapters.fetch, apiBase, jwt, found.id);
|
|
451
|
+
return "token" in minted ? minted.token : null;
|
|
452
|
+
} catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** The repo's most recently updated open PRs (any author — the person's own
|
|
458
|
+
* included). Empty when GitHub cannot be read. */
|
|
459
|
+
export async function listOpenPrs(
|
|
460
|
+
fetchFn: typeof fetch,
|
|
461
|
+
apiBase: string,
|
|
462
|
+
token: string,
|
|
463
|
+
repo: RepoSlug,
|
|
464
|
+
limit = PR_OFFER_SIZE,
|
|
465
|
+
): Promise<OpenPr[]> {
|
|
466
|
+
const r = await api(
|
|
467
|
+
fetchFn,
|
|
468
|
+
`${apiBase}/repos/${repo.owner}/${repo.name}/pulls?state=open&sort=updated&direction=desc&per_page=${limit}`,
|
|
469
|
+
{ headers: { authorization: `Bearer ${token}` } },
|
|
470
|
+
);
|
|
471
|
+
if (r.status !== 200 || !Array.isArray(r.body)) return [];
|
|
472
|
+
return (r.body as Array<Record<string, unknown>>)
|
|
473
|
+
.filter((p) => typeof p.number === "number")
|
|
474
|
+
.slice(0, limit)
|
|
475
|
+
.map((p) => ({
|
|
476
|
+
number: p.number as number,
|
|
477
|
+
title: typeof p.title === "string" ? p.title : "",
|
|
478
|
+
author: textOf((p.user as { login?: unknown } | null)?.login),
|
|
479
|
+
updated_at: typeof p.updated_at === "string" ? p.updated_at : "",
|
|
480
|
+
draft: p.draft === true,
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
483
|
+
|
|
297
484
|
export function proofRoundStep(opts: ProofStepOptions = {}): OnboardingStep {
|
|
298
485
|
const pollAttempts = opts.pollAttempts ?? 60;
|
|
299
486
|
const pollIntervalMs = opts.pollIntervalMs ?? 30_000;
|
|
300
487
|
const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
|
|
301
488
|
const dryrunLimit = opts.dryrunLimit ?? 40;
|
|
489
|
+
const apiBase = opts.apiBase ?? "https://api.github.com";
|
|
490
|
+
const resultAttempts = opts.resultAttempts ?? 15;
|
|
491
|
+
const resultIntervalMs = opts.resultIntervalMs ?? 2_000;
|
|
302
492
|
|
|
303
493
|
function log(ctx: StepContext, line: string): void {
|
|
304
494
|
ctx.adapters.log?.(`${PROOF_STEP_ID}: ${line}`);
|
|
@@ -322,23 +512,132 @@ export function proofRoundStep(opts: ProofStepOptions = {}): OnboardingStep {
|
|
|
322
512
|
: { obs: null, detail: `\`qcr why\` did not answer JSON: ${r.stdout.slice(0, 200)}` };
|
|
323
513
|
}
|
|
324
514
|
|
|
515
|
+
/** Send `review-now` for `pr` and wait for the daemon's receipt. `refused`
|
|
516
|
+
* means the daemon read the command and said no (its reason is the answer);
|
|
517
|
+
* otherwise a failure is the channel itself (write or no receipt). */
|
|
518
|
+
async function requestReview(
|
|
519
|
+
ctx: StepContext,
|
|
520
|
+
transport: StackTransport,
|
|
521
|
+
pr: number,
|
|
522
|
+
): Promise<{ ok: boolean; refused: boolean; detail: string }> {
|
|
523
|
+
const name = reviewNowFileName(ctx.adapters.clock(), pr);
|
|
524
|
+
const write = await transport.execAsRoot(reviewNowWriteArgv(name, pr));
|
|
525
|
+
if (write.code !== 0)
|
|
526
|
+
return {
|
|
527
|
+
ok: false,
|
|
528
|
+
refused: false,
|
|
529
|
+
detail: `the command write failed: ${(write.stderr || write.stdout).trim().slice(0, 200) || `exit ${write.code}`}`,
|
|
530
|
+
};
|
|
531
|
+
for (let attempt = 1; attempt <= resultAttempts; attempt += 1) {
|
|
532
|
+
await sleep(resultIntervalMs);
|
|
533
|
+
const read = await transport.exec(reviewNowResultArgv(name));
|
|
534
|
+
const body = read.code === 0 ? read.stdout.trim() : "";
|
|
535
|
+
if (body === "" || body === "pending") continue;
|
|
536
|
+
let v: Record<string, unknown>;
|
|
537
|
+
try {
|
|
538
|
+
v = JSON.parse(body) as Record<string, unknown>;
|
|
539
|
+
} catch {
|
|
540
|
+
continue; // the daemon may still be writing its receipt
|
|
541
|
+
}
|
|
542
|
+
if (v.ok === true)
|
|
543
|
+
return {
|
|
544
|
+
ok: true,
|
|
545
|
+
refused: false,
|
|
546
|
+
detail: `queued${typeof v.state === "string" ? `, state ${v.state}` : ""}`,
|
|
547
|
+
};
|
|
548
|
+
return {
|
|
549
|
+
ok: false,
|
|
550
|
+
refused: true,
|
|
551
|
+
detail:
|
|
552
|
+
typeof v.reason === "string"
|
|
553
|
+
? v.reason.replace(/\s+/g, " ").slice(0, 300)
|
|
554
|
+
: "no reason given",
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
ok: false,
|
|
559
|
+
refused: false,
|
|
560
|
+
detail: `no answer to ${name} after ${resultAttempts} reads — the daemon may still be starting`,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** No PR yet: offer the repo's open PRs, and say what happens next. */
|
|
565
|
+
async function askCheck(
|
|
566
|
+
ctx: StepContext,
|
|
567
|
+
repo: string,
|
|
568
|
+
label: string,
|
|
569
|
+
posture: string | null,
|
|
570
|
+
typed: boolean,
|
|
571
|
+
): Promise<CheckResult> {
|
|
572
|
+
const slug = parseRepoSlug(repo);
|
|
573
|
+
let offer: OpenPr[] = [];
|
|
574
|
+
if (slug) {
|
|
575
|
+
const token = await readToken(
|
|
576
|
+
ctx,
|
|
577
|
+
slug,
|
|
578
|
+
apiBase,
|
|
579
|
+
opts.secretsDir ?? secretsDirFor(ctx.adapters, ctx.choices),
|
|
580
|
+
);
|
|
581
|
+
if (token) offer = await listOpenPrs(ctx.adapters.fetch, apiBase, token, slug);
|
|
582
|
+
}
|
|
583
|
+
const me = textOf(ctx.choices[CHOICE_KEYS.accountLogin]);
|
|
584
|
+
const rows = offer.map(
|
|
585
|
+
(p) =>
|
|
586
|
+
` #${p.number} ${p.title}${p.draft ? " (draft)" : ""} — by ${p.author}${me !== "" && p.author === me ? " (yours)" : ""}, updated ${p.updated_at}`,
|
|
587
|
+
);
|
|
588
|
+
const assurance =
|
|
589
|
+
posture === "true"
|
|
590
|
+
? "Nothing will be posted to GitHub: dry_run is on, so every would-be comment and review is written to a file instead, and you will read it here before deciding anything."
|
|
591
|
+
: posture === "false"
|
|
592
|
+
? "This installation is LIVE (dry_run = false): the review will be posted to GitHub. The flag belongs to you; these instructions do not change it."
|
|
593
|
+
: "The dry_run posture could not be read, so posting is unknown. Check the live config before reviewing a PR.";
|
|
594
|
+
return {
|
|
595
|
+
name: "ask",
|
|
596
|
+
ok: false,
|
|
597
|
+
detail:
|
|
598
|
+
(typed ? "That is not a pull request number or link.\n" : "") +
|
|
599
|
+
"Read this out, then call this tool again with the PR number (pr) or its link (url).\n\n" +
|
|
600
|
+
(rows.length > 0
|
|
601
|
+
? `The ${rows.length} most recently updated open pull requests in ${repo}:\n${rows.join("\n")}\n` +
|
|
602
|
+
"Pick one of these — or any other open PR in it — one you would actually want reviewed.\n"
|
|
603
|
+
: `Pick a real, open pull request in ${repo} — one you would actually want reviewed (your own is fine).\n`) +
|
|
604
|
+
"Its review starts as soon as you name it: nothing to add to the PR. (For later reviews, the trigger is the label " +
|
|
605
|
+
`"${label}" — the reviewer never reviews anything on its own initiative.)\n` +
|
|
606
|
+
assurance,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
325
610
|
return {
|
|
326
611
|
id: PROOF_STEP_ID,
|
|
327
612
|
title: "The proof: watch a real PR get reviewed",
|
|
328
613
|
explain:
|
|
329
|
-
"The wizard does not call itself done until you have seen the reviewer work
|
|
330
|
-
"
|
|
331
|
-
"
|
|
332
|
-
"
|
|
333
|
-
"
|
|
614
|
+
"The wizard does not call itself done until you have seen the reviewer work, on one real, open pull request. Call this " +
|
|
615
|
+
"tool with no arguments to get the repo's five most recently updated open PRs (the operator's own included) to pick " +
|
|
616
|
+
"from, then call it again with pr (a number) or url (the PR's link). The review starts IMMEDIATELY — no label to add: " +
|
|
617
|
+
"the wizard asks the daemon directly. It follows the round through the daemon's own state until a verdict lands, then " +
|
|
618
|
+
"SHOWS the review — the verdict, how many findings, the top ones, and where the full text is — read out of the " +
|
|
619
|
+
"dry-run record, so they judge it while it is still impossible for it to have posted to GitHub. If nothing happens, " +
|
|
620
|
+
"or the round fails, it says what to check rather than waiting forever.",
|
|
334
621
|
order: 76,
|
|
622
|
+
// After setup: the board offers it; finish never waits on it.
|
|
623
|
+
optional: true,
|
|
335
624
|
parameters: {
|
|
336
625
|
type: "object",
|
|
337
626
|
properties: {
|
|
338
627
|
pr: {
|
|
339
628
|
type: "integer",
|
|
340
629
|
description:
|
|
341
|
-
"The pull request number
|
|
630
|
+
"The pull request number to review now. Call with no arguments first to get the open PRs to choose from.",
|
|
631
|
+
},
|
|
632
|
+
url: {
|
|
633
|
+
type: "string",
|
|
634
|
+
description:
|
|
635
|
+
"The pull request's URL, instead of its number (github.com/<owner>/<repo>/pull/<n>).",
|
|
636
|
+
},
|
|
637
|
+
confirm_live: {
|
|
638
|
+
type: "string",
|
|
639
|
+
description:
|
|
640
|
+
'Only when the installation is LIVE (dry_run = false): "yes" means the operator agreed that this review posts to GitHub.',
|
|
342
641
|
},
|
|
343
642
|
},
|
|
344
643
|
},
|
|
@@ -347,20 +646,19 @@ export function proofRoundStep(opts: ProofStepOptions = {}): OnboardingStep {
|
|
|
347
646
|
const transport = stackTransportFor(ctx, opts.transport);
|
|
348
647
|
const repo = repoOf(ctx.choices, ctx.adapters);
|
|
349
648
|
const label = triggerLabelOf(ctx.adapters);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
: "The dry_run posture could not be read, so posting is unknown. Check the live config before adding the trigger label.";
|
|
649
|
+
let posture: string | null = null;
|
|
650
|
+
try {
|
|
651
|
+
posture = String(loadConfig(configPath(ctx.adapters)).dry_run);
|
|
652
|
+
} catch {
|
|
653
|
+
/* Unknown posture stays explicit. */
|
|
654
|
+
}
|
|
655
|
+
const answer = prFrom(ctx.input, ctx.choices);
|
|
656
|
+
const typed = ctx.input.pr !== undefined || ctx.input.url !== undefined;
|
|
657
|
+
if (answer === null || (typed && parsePrAnswer(ctx.input.pr ?? ctx.input.url) === null)) {
|
|
658
|
+
return { ok: false, checks: [await askCheck(ctx, repo, label, posture, typed)] };
|
|
659
|
+
}
|
|
660
|
+
const pr = answer.pr;
|
|
661
|
+
if (answer.repo !== null && answer.repo.toLowerCase() !== repo.toLowerCase()) {
|
|
364
662
|
return {
|
|
365
663
|
ok: false,
|
|
366
664
|
checks: [
|
|
@@ -368,10 +666,25 @@ export function proofRoundStep(opts: ProofStepOptions = {}): OnboardingStep {
|
|
|
368
666
|
name: "ask",
|
|
369
667
|
ok: false,
|
|
370
668
|
detail:
|
|
371
|
-
`
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
669
|
+
`that link is a pull request in ${answer.repo}, but this reviewer reviews ${repo}. Pick an open PR in ${repo} ` +
|
|
670
|
+
"(or change the reviewed repo in the earlier GitHub step)",
|
|
671
|
+
},
|
|
672
|
+
],
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
if (posture !== "true" && textOf(ctx.input.confirm_live).trim().toLowerCase() !== "yes") {
|
|
676
|
+
return {
|
|
677
|
+
ok: false,
|
|
678
|
+
checks: [
|
|
679
|
+
{
|
|
680
|
+
name: "live",
|
|
681
|
+
ok: false,
|
|
682
|
+
detail:
|
|
683
|
+
posture === "false"
|
|
684
|
+
? `This installation is LIVE (dry_run = false): reviewing #${pr} now POSTS the review to GitHub. Ask the operator; ` +
|
|
685
|
+
'only on their yes, call this step again with confirm_live: "yes". The flag is theirs; this step never changes it.'
|
|
686
|
+
: `The dry_run posture could not be read (${configPath(ctx.adapters)}), so whether this review would post is unknown. ` +
|
|
687
|
+
'Fix the config first — or, on the operator\'s explicit yes, call again with confirm_live: "yes".',
|
|
375
688
|
},
|
|
376
689
|
],
|
|
377
690
|
};
|
|
@@ -391,28 +704,73 @@ export function proofRoundStep(opts: ProofStepOptions = {}): OnboardingStep {
|
|
|
391
704
|
};
|
|
392
705
|
}
|
|
393
706
|
|
|
707
|
+
// Where the PR stands before anything is asked of the daemon.
|
|
708
|
+
const first = await observe(transport, pr);
|
|
709
|
+
if (first.obs === null)
|
|
710
|
+
return { ok: false, checks: [stateStoreCheck(first.detail, transport)] };
|
|
711
|
+
const before = first.obs.rounds.at(-1);
|
|
712
|
+
const recorded = parseProofRecord(ctx.choices[PROOF_CHOICE_KEY]);
|
|
713
|
+
const resumable =
|
|
714
|
+
recorded !== null &&
|
|
715
|
+
recorded.pr === pr &&
|
|
716
|
+
before !== undefined &&
|
|
717
|
+
before.round_no === recorded.round_no &&
|
|
718
|
+
completedRound(before);
|
|
719
|
+
const inFlight =
|
|
720
|
+
before?.status === "running" ||
|
|
721
|
+
first.obs.state === "queued" ||
|
|
722
|
+
first.obs.state === "reviewing";
|
|
723
|
+
const initial = classifyRounds(first.obs);
|
|
394
724
|
log(
|
|
395
725
|
ctx,
|
|
396
|
-
|
|
726
|
+
initial.kind === "waiting"
|
|
727
|
+
? initial.detail
|
|
728
|
+
: `#${pr} has ${first.obs.rounds.length} round${first.obs.rounds.length === 1 ? "" : "s"} on record (newest: round ${before?.round_no}, ${before?.status})`,
|
|
397
729
|
);
|
|
398
|
-
|
|
399
|
-
for
|
|
400
|
-
|
|
401
|
-
|
|
730
|
+
|
|
731
|
+
// Ask for the review — unless one is already under way, or this is a
|
|
732
|
+
// re-run that only has to show the round it already recorded.
|
|
733
|
+
let requested = false;
|
|
734
|
+
let requestNote = "";
|
|
735
|
+
if (!resumable && !inFlight) {
|
|
736
|
+
const sent = await requestReview(ctx, transport, pr);
|
|
737
|
+
requested = sent.ok;
|
|
738
|
+
requestNote = sent.detail;
|
|
739
|
+
if (!sent.ok && sent.refused)
|
|
402
740
|
return {
|
|
403
741
|
ok: false,
|
|
404
742
|
checks: [
|
|
405
743
|
{
|
|
406
|
-
name: "
|
|
744
|
+
name: "review-now",
|
|
407
745
|
ok: false,
|
|
408
746
|
detail:
|
|
409
|
-
`
|
|
410
|
-
|
|
747
|
+
`the daemon refused to review #${pr} now: ${sent.detail}. ` +
|
|
748
|
+
`If this daemon predates "review this one PR now", add the label "${label}" to the PR instead and run this step again — ` +
|
|
749
|
+
"the label is the trigger the daemon has always understood.",
|
|
411
750
|
},
|
|
412
751
|
],
|
|
413
752
|
};
|
|
414
|
-
|
|
415
|
-
|
|
753
|
+
log(
|
|
754
|
+
ctx,
|
|
755
|
+
sent.ok
|
|
756
|
+
? `asked the daemon to review #${pr} now (${sent.detail}) — no label needed`
|
|
757
|
+
: `could not ask the daemon directly (${sent.detail}); following #${pr} in case it is labelled "${label}"`,
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
// Only a round NEWER than the ones before the request answers it.
|
|
761
|
+
const floor = requested ? (before?.round_no ?? 0) : -Infinity;
|
|
762
|
+
|
|
763
|
+
log(
|
|
764
|
+
ctx,
|
|
765
|
+
`following #${pr} — checking the daemon's state every ${Math.round(pollIntervalMs / 1000)}s, up to ${pollAttempts} times`,
|
|
766
|
+
);
|
|
767
|
+
let last = "";
|
|
768
|
+
let settledPolls = 0;
|
|
769
|
+
for (let attempt = 1; attempt <= pollAttempts; attempt += 1) {
|
|
770
|
+
const { obs, detail } = await observe(transport, pr);
|
|
771
|
+
if (obs === null) return { ok: false, checks: [stateStoreCheck(detail, transport)] };
|
|
772
|
+
const fresh = { ...obs, rounds: obs.rounds.filter((r) => r.round_no > floor) };
|
|
773
|
+
const outcome = classifyRounds(fresh);
|
|
416
774
|
if (outcome.kind === "done") {
|
|
417
775
|
log(ctx, `round ${outcome.round.round_no} finished with ${outcome.round.verdict}`);
|
|
418
776
|
return finish(ctx, transport, pr, outcome.round);
|
|
@@ -420,16 +778,47 @@ export function proofRoundStep(opts: ProofStepOptions = {}): OnboardingStep {
|
|
|
420
778
|
if (outcome.kind === "failed") {
|
|
421
779
|
return { ok: false, checks: [roundFailedCheck(outcome.round, pr, transport)] };
|
|
422
780
|
}
|
|
781
|
+
// Asked, but the daemon settled the PR without a new round (nothing
|
|
782
|
+
// changed since a review of this same head): that review is the answer.
|
|
783
|
+
if (
|
|
784
|
+
requested &&
|
|
785
|
+
outcome.kind === "waiting" &&
|
|
786
|
+
obs.state !== "queued" &&
|
|
787
|
+
obs.state !== "reviewing"
|
|
788
|
+
) {
|
|
789
|
+
settledPolls += 1;
|
|
790
|
+
const standing = obs.rounds.at(-1);
|
|
791
|
+
if (settledPolls >= 2 && completedRound(standing)) {
|
|
792
|
+
log(
|
|
793
|
+
ctx,
|
|
794
|
+
`the daemon reviewed nothing new — #${pr}'s head was already reviewed in round ${standing.round_no}; showing that review`,
|
|
795
|
+
);
|
|
796
|
+
return finish(ctx, transport, pr, standing);
|
|
797
|
+
}
|
|
798
|
+
} else settledPolls = 0;
|
|
423
799
|
last =
|
|
424
800
|
outcome.kind === "running"
|
|
425
801
|
? `round ${outcome.round.round_no} is running (started ${outcome.round.started_at})`
|
|
426
|
-
:
|
|
802
|
+
: requested && obs.state === "queued"
|
|
803
|
+
? "queued — the round starts as soon as a review slot is free"
|
|
804
|
+
: outcome.detail;
|
|
427
805
|
log(ctx, `${last} — attempt ${attempt}/${pollAttempts}`);
|
|
428
806
|
if (attempt < pollAttempts) await sleep(pollIntervalMs);
|
|
429
807
|
}
|
|
430
808
|
return {
|
|
431
809
|
ok: false,
|
|
432
|
-
checks: [
|
|
810
|
+
checks: [
|
|
811
|
+
timeoutCheck(
|
|
812
|
+
pr,
|
|
813
|
+
repo,
|
|
814
|
+
label,
|
|
815
|
+
last,
|
|
816
|
+
pollAttempts,
|
|
817
|
+
pollIntervalMs,
|
|
818
|
+
transport,
|
|
819
|
+
requested ? requestNote : null,
|
|
820
|
+
),
|
|
821
|
+
],
|
|
433
822
|
};
|
|
434
823
|
},
|
|
435
824
|
|
|
@@ -537,7 +926,19 @@ export function proofRoundStep(opts: ProofStepOptions = {}): OnboardingStep {
|
|
|
537
926
|
return { ok: false, checks };
|
|
538
927
|
}
|
|
539
928
|
const other = records.filter((r) => r.kind !== "create_review").length;
|
|
540
|
-
|
|
929
|
+
const file = records
|
|
930
|
+
.filter((r) => r.kind === "create_review" && r.reviewPurpose === undefined)
|
|
931
|
+
.at(-1)?.file;
|
|
932
|
+
checks.push({
|
|
933
|
+
name: "review",
|
|
934
|
+
ok: true,
|
|
935
|
+
detail: renderReview(review, pr, {
|
|
936
|
+
verdict: round.verdict,
|
|
937
|
+
...(file
|
|
938
|
+
? { file, readCommand: transport.execCommand(`cat ${CONTAINER_DRYRUN_DIR}/${file}`) }
|
|
939
|
+
: {}),
|
|
940
|
+
}),
|
|
941
|
+
});
|
|
541
942
|
checks.push({
|
|
542
943
|
name: "nothing-posted",
|
|
543
944
|
ok: true,
|
|
@@ -596,6 +997,16 @@ function roundFailedCheck(
|
|
|
596
997
|
};
|
|
597
998
|
}
|
|
598
999
|
|
|
1000
|
+
function stateStoreCheck(detail: string, transport: StackTransport): CheckResult {
|
|
1001
|
+
return {
|
|
1002
|
+
name: "state-store",
|
|
1003
|
+
ok: false,
|
|
1004
|
+
detail:
|
|
1005
|
+
`could not read the daemon's state (${detail}) — Fix: \`${transport.logsCommand(200)}\`. The daemon ` +
|
|
1006
|
+
"may still be starting (the image is prebuilt; its first boot migrates state and reconciles), so give it a minute after bring-up and run this step again",
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
|
|
599
1010
|
function timeoutCheck(
|
|
600
1011
|
pr: number,
|
|
601
1012
|
repo: string,
|
|
@@ -604,6 +1015,8 @@ function timeoutCheck(
|
|
|
604
1015
|
attempts: number,
|
|
605
1016
|
intervalMs: number,
|
|
606
1017
|
transport: StackTransport,
|
|
1018
|
+
/** Set when the review was asked for directly (review-now): its receipt. */
|
|
1019
|
+
requested: string | null = null,
|
|
607
1020
|
): CheckResult {
|
|
608
1021
|
const minutes = Math.round((attempts * intervalMs) / 60_000);
|
|
609
1022
|
return {
|
|
@@ -612,6 +1025,11 @@ function timeoutCheck(
|
|
|
612
1025
|
detail:
|
|
613
1026
|
`no finished round for #${pr} after ${minutes} minutes of watching (last seen: ${last || "nothing at all"}). ` +
|
|
614
1027
|
"Nothing is broken by this — the wizard simply stopped waiting, and running this step again picks the watch back up.\n" +
|
|
1028
|
+
(requested !== null
|
|
1029
|
+
? `The review WAS asked for directly (the daemon answered: ${requested}), so no label is needed. A first round on a ` +
|
|
1030
|
+
"big PR can take a while; `qcr tui " +
|
|
1031
|
+
`${pr}\` shows the round's own live log. If it never started:\n`
|
|
1032
|
+
: "") +
|
|
615
1033
|
"What to check, in the order that actually finds it:\n" +
|
|
616
1034
|
`① the label on #${pr} must be exactly "${label}" — the daemon matches the name from [github] trigger_label, and a ` +
|
|
617
1035
|
"near-miss looks like nothing happening;\n" +
|