@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
|
@@ -81,7 +81,7 @@ export interface TopologyOption {
|
|
|
81
81
|
const TOPOLOGY_OPTION_METADATA = {
|
|
82
82
|
vm: {
|
|
83
83
|
headline:
|
|
84
|
-
"A VM or server over ssh — RECOMMENDED, because a reviewer that is asleep reviews nothing",
|
|
84
|
+
"A VM or server over ssh — RECOMMENDED when this computer sleeps (a laptop), because a reviewer that is asleep reviews nothing",
|
|
85
85
|
tradeoffs: [
|
|
86
86
|
"it is on 24/7: pull requests opened while you sleep, while your laptop is shut, or while you are on a plane still get reviewed",
|
|
87
87
|
"you bring the box (any ssh-reachable Linux machine — a cheap cloud VM is plenty); the wizard does not create one for you",
|
|
@@ -91,12 +91,13 @@ const TOPOLOGY_OPTION_METADATA = {
|
|
|
91
91
|
],
|
|
92
92
|
},
|
|
93
93
|
local: {
|
|
94
|
-
headline:
|
|
94
|
+
headline:
|
|
95
|
+
"This computer — everything in Docker on the machine this wizard runs on; THE pick when it is itself an always-on server (a cloud VM you are logged into)",
|
|
95
96
|
tradeoffs: [
|
|
96
|
-
"nothing else to own: `docker compose up` and the whole system is here — daemon
|
|
97
|
-
"it reviews only while this machine is
|
|
98
|
-
"
|
|
99
|
-
"the honest way to try QuickReview, and a perfectly good permanent setup
|
|
97
|
+
"nothing else to own: `docker compose up` and the whole system is here — daemon and memory engine, and the TUI reads them locally with no ssh",
|
|
98
|
+
"it reviews only while this machine is on and the containers are running: on a server that is always; on a laptop, close the lid and the reviewer stops",
|
|
99
|
+
"nothing to expose on the default path: the daemon polls GitHub for new pull requests every few minutes, with no webhook, tunnel or public URL",
|
|
100
|
+
"the honest way to try QuickReview, and a perfectly good permanent setup on a desktop that stays on or a cloud VM",
|
|
100
101
|
"you can switch to a server later by re-running this step — the answers you gave (repo, App, keys) are kept",
|
|
101
102
|
],
|
|
102
103
|
},
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
composePs,
|
|
38
38
|
containerStateCheck,
|
|
39
39
|
daemonExecArgs,
|
|
40
|
+
daemonRootExecArgs,
|
|
40
41
|
healthUrl,
|
|
41
42
|
memoryOn,
|
|
42
43
|
isUp,
|
|
@@ -87,6 +88,10 @@ export interface StackTransport {
|
|
|
87
88
|
daemonState(): Promise<DaemonState>;
|
|
88
89
|
/** Run an argv INSIDE the daemon container. */
|
|
89
90
|
exec(argv: readonly string[]): Promise<ProcessResult>;
|
|
91
|
+
/** The same, as uid 0: the one way to write the root-sealed operator
|
|
92
|
+
* command directory (`commands/operator/`), as `qcr drain` and the TUI's
|
|
93
|
+
* levers do. Used for the operator's own commands only. */
|
|
94
|
+
execAsRoot(argv: readonly string[]): Promise<ProcessResult>;
|
|
90
95
|
/** Run an argv INSIDE the container that owns the memory engine: the memory
|
|
91
96
|
* sidecar on the compose stack, `[tui] memory_container` (else the daemon's
|
|
92
97
|
* container) on the server route. */
|
|
@@ -155,6 +160,10 @@ function composeTransport(ctx: StepContext, opts: TransportOptions): StackTransp
|
|
|
155
160
|
return ctx.adapters.runner.run("docker", daemonExecArgs(root, argv));
|
|
156
161
|
},
|
|
157
162
|
|
|
163
|
+
async execAsRoot(argv) {
|
|
164
|
+
return ctx.adapters.runner.run("docker", daemonRootExecArgs(root, argv));
|
|
165
|
+
},
|
|
166
|
+
|
|
158
167
|
async execMemory(argv) {
|
|
159
168
|
return ctx.adapters.runner.run(
|
|
160
169
|
"docker",
|
|
@@ -465,6 +474,12 @@ function sshTransport(ctx: StepContext, opts: TransportOptions): StackTransport
|
|
|
465
474
|
return remote([...dockerPrefix, "exec", facts.container, ...argv]);
|
|
466
475
|
},
|
|
467
476
|
|
|
477
|
+
async execAsRoot(argv) {
|
|
478
|
+
if (facts.alias === "")
|
|
479
|
+
return { code: 1, stdout: "", stderr: "no ssh alias recorded for the server route" };
|
|
480
|
+
return remote([...dockerPrefix, "exec", "-u", "0", facts.container, ...argv]);
|
|
481
|
+
},
|
|
482
|
+
|
|
468
483
|
async execMemory(argv) {
|
|
469
484
|
if (facts.alias === "")
|
|
470
485
|
return { code: 1, stdout: "", stderr: "no ssh alias recorded for the server route" };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// The TUI's own route, checked by the layered verification.
|
|
2
|
+
//
|
|
3
|
+
// The containers being up and the daemon answering /health prove the stack;
|
|
4
|
+
// they do not prove that `qcr tui`, opened right after the wizard, can READ
|
|
5
|
+
// it. The TUI reads status.json its own way — the [tui] block, resolved by
|
|
6
|
+
// src/tui/route.ts, through `runnerFor` (local docker, or ssh to the VM) — so
|
|
7
|
+
// this check runs exactly that: the same resolution, the same argv
|
|
8
|
+
// (`statusArgv`), the same runner, and it needs a status.json that parses.
|
|
9
|
+
// A red check here is the board that would open on "✖ unreachable".
|
|
10
|
+
import { loadConfig, type TuiConfig } from "../../config.js";
|
|
11
|
+
import { runnerFor, statusArgv, transportOf, type SshRunner } from "../../tui/remote.js";
|
|
12
|
+
import { readRouteFacts, resolveTuiRoute, type RouteFacts } from "../../tui/route.js";
|
|
13
|
+
import { sandboxRoot, type CheckResult, type StepContext } from "../engine.js";
|
|
14
|
+
|
|
15
|
+
export const TUI_ROUTE_CHECK = "tui-route";
|
|
16
|
+
|
|
17
|
+
export interface TuiRouteCheckOptions {
|
|
18
|
+
/** The runner for a resolved route; default `runnerFor` (spawns docker/ssh). */
|
|
19
|
+
runner?: (cfg: TuiConfig) => SshRunner;
|
|
20
|
+
/** Route facts that override what is read from the sandbox root. */
|
|
21
|
+
facts?: Partial<RouteFacts>;
|
|
22
|
+
/** Attempts while status.json is not written yet (a daemon just booted). */
|
|
23
|
+
attempts?: number;
|
|
24
|
+
intervalMs?: number;
|
|
25
|
+
sleep?: (ms: number) => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function firstLine(text: string): string {
|
|
29
|
+
return (
|
|
30
|
+
text
|
|
31
|
+
.split("\n")
|
|
32
|
+
.map((l) => l.trim())
|
|
33
|
+
.find((l) => l !== "") ?? ""
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Total: every outcome is a check, never a throw. */
|
|
38
|
+
export async function tuiRouteCheck(
|
|
39
|
+
ctx: StepContext,
|
|
40
|
+
opts: TuiRouteCheckOptions = {},
|
|
41
|
+
): Promise<CheckResult> {
|
|
42
|
+
if (ctx.adapters.host.inContainer) {
|
|
43
|
+
return {
|
|
44
|
+
name: TUI_ROUTE_CHECK,
|
|
45
|
+
ok: true,
|
|
46
|
+
detail:
|
|
47
|
+
"not checked here — this wizard runs inside the image, and the TUI runs on the host: open `qcr tui` there",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
let tui: TuiConfig;
|
|
51
|
+
try {
|
|
52
|
+
tui = loadConfig(ctx.adapters.paths.configPath).tui;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
return {
|
|
55
|
+
name: TUI_ROUTE_CHECK,
|
|
56
|
+
ok: false,
|
|
57
|
+
detail: `config.toml does not load, so the TUI could not start either: ${String(err instanceof Error ? err.message : err)}`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const route = resolveTuiRoute(tui, {
|
|
61
|
+
...readRouteFacts(sandboxRoot(ctx.adapters.paths)),
|
|
62
|
+
...opts.facts,
|
|
63
|
+
});
|
|
64
|
+
if (route.unconfigured !== undefined)
|
|
65
|
+
return {
|
|
66
|
+
name: TUI_ROUTE_CHECK,
|
|
67
|
+
ok: false,
|
|
68
|
+
detail: `${route.unconfigured} — Fix: run the config_fill step again`,
|
|
69
|
+
};
|
|
70
|
+
const cfg = route.cfg;
|
|
71
|
+
const run = (opts.runner ?? runnerFor)(cfg);
|
|
72
|
+
const argv = statusArgv(cfg);
|
|
73
|
+
const where =
|
|
74
|
+
transportOf(cfg) === "local"
|
|
75
|
+
? `local docker, container ${cfg.container}`
|
|
76
|
+
: `ssh ${cfg.ssh_host}`;
|
|
77
|
+
const attempts = Math.max(1, opts.attempts ?? 4);
|
|
78
|
+
const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
|
|
79
|
+
let last = "";
|
|
80
|
+
for (let i = 1; i <= attempts; i++) {
|
|
81
|
+
let res;
|
|
82
|
+
try {
|
|
83
|
+
res = await run(cfg.ssh_host, argv, { timeoutMs: 15_000 });
|
|
84
|
+
} catch (err) {
|
|
85
|
+
res = { ok: false, stdout: "", stderr: String(err), code: null };
|
|
86
|
+
}
|
|
87
|
+
if (res.ok) {
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(res.stdout) as unknown;
|
|
90
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
91
|
+
const prs = (parsed as { prs?: unknown }).prs;
|
|
92
|
+
return {
|
|
93
|
+
name: TUI_ROUTE_CHECK,
|
|
94
|
+
ok: true,
|
|
95
|
+
detail:
|
|
96
|
+
`the TUI's own read works (${where}: \`${argv.join(" ")}\`) — status.json parses` +
|
|
97
|
+
`${Array.isArray(prs) ? `, ${prs.length} PR${prs.length === 1 ? "" : "s"} on the board` : ""}`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
last = "status.json is not a JSON object";
|
|
101
|
+
} catch {
|
|
102
|
+
last = "status.json is not valid JSON";
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
last = firstLine(res.stderr) || `exit ${String(res.code)}`;
|
|
106
|
+
}
|
|
107
|
+
if (i < attempts) await sleep(opts.intervalMs ?? 5_000);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
name: TUI_ROUTE_CHECK,
|
|
111
|
+
ok: false,
|
|
112
|
+
detail:
|
|
113
|
+
`the TUI could not read status.json the way it will (${where}: \`${argv.join(" ")}\`): ${last} — Fix: ` +
|
|
114
|
+
(transportOf(cfg) === "local"
|
|
115
|
+
? "the [tui] container must be the running daemon container and remote_state_dir its state dir " +
|
|
116
|
+
"(run the config_fill step again to rewrite both)"
|
|
117
|
+
: `\`ssh ${cfg.ssh_host} true\` must work without a prompt, and remote_state_dir must be the VM's state dir`),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
// server, a curl on the box). Layer 3 needs no transport at all: it reads
|
|
35
35
|
// GitHub's OWN delivery log, which is the same record wherever the daemon
|
|
36
36
|
// runs, and that is exactly why it is the layer that proves the path.
|
|
37
|
-
import { CHOICE_KEYS } from "../choices.js";
|
|
37
|
+
import { CHOICE_KEYS, userAuthRecorded } from "../choices.js";
|
|
38
38
|
import { mintAppJwt } from "@quickchatai/pi-quickreview/src/githubAuth.js";
|
|
39
39
|
import type { CheckResult, OnboardingStep, StepContext, StepReport } from "../engine.js";
|
|
40
40
|
import { ingressOptionOf, secretsDirFor, readConfigText } from "./compose.js";
|
|
@@ -42,6 +42,7 @@ import { api as githubApi, readCredentials } from "./githubapp.js";
|
|
|
42
42
|
import { readTomlKey, WEBHOOK_PATH } from "./ingress.js";
|
|
43
43
|
import { sshCommandLine } from "./ssh.js";
|
|
44
44
|
import { sshAliasOf, topologyOf } from "./topology.js";
|
|
45
|
+
import { tuiRouteCheck, type TuiRouteCheckOptions } from "./tuiroute.js";
|
|
45
46
|
import {
|
|
46
47
|
DEFAULT_HEALTH_TIMEOUT_MS,
|
|
47
48
|
stackTransportFor,
|
|
@@ -67,6 +68,8 @@ export interface VerifyStackStepOptions {
|
|
|
67
68
|
* WHICH transport is never an option: `stackTransportFor` decides from the
|
|
68
69
|
* recorded topology, and this step never learns which one it got. */
|
|
69
70
|
transport?: TransportOptions;
|
|
71
|
+
/** The TUI-route check's seams (tuiroute.ts): its runner and route facts. */
|
|
72
|
+
tuiRoute?: TuiRouteCheckOptions;
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
// ---- GitHub's delivery log ---------------------------------------------------
|
|
@@ -415,6 +418,22 @@ export function verifyStackStep(opts: VerifyStackStepOptions = {}): OnboardingSt
|
|
|
415
418
|
});
|
|
416
419
|
return { ok: false, checks };
|
|
417
420
|
}
|
|
421
|
+
// The operator's own window onto the stack: can `qcr tui` read it the way
|
|
422
|
+
// it will? Independent of webhook delivery, so it never stops layer 3.
|
|
423
|
+
checks.push(await tuiRouteCheck(ctx, opts.tuiRoute));
|
|
424
|
+
// Layer 3 reads the App's own hook config and delivery log — App-JWT
|
|
425
|
+
// calls. The own-account route has no App, so there is no hook to read
|
|
426
|
+
// and nothing to deliver: the layer is skipped, and the reason said.
|
|
427
|
+
if (userAuthRecorded(ctx.choices)) {
|
|
428
|
+
checks.push({
|
|
429
|
+
name: "ping-delivery",
|
|
430
|
+
ok: true,
|
|
431
|
+
detail:
|
|
432
|
+
'not checked — this install runs on your own GitHub account ([github] auth = "user"), which has no GitHub App, ' +
|
|
433
|
+
"so there is no webhook and no App delivery log to read; the reconcile poll is the only trigger path",
|
|
434
|
+
});
|
|
435
|
+
return { ok: checks.every((c) => c.ok), checks };
|
|
436
|
+
}
|
|
418
437
|
// Layer 3 exists only where GitHub can deliver. The polling topology
|
|
419
438
|
// deliberately exposes nothing, so a ping can never land there — requiring
|
|
420
439
|
// one would keep this step (and so onboarding's finish) red forever.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The onboarding session's tool names: one source for the extension that
|
|
2
|
+
// registers them (extensions/onboarding.ts) and the argv that activates them
|
|
3
|
+
// (launch.ts `onboardingPiArgv`).
|
|
4
|
+
//
|
|
5
|
+
// pi's `--tools <list>` is an allowlist over EVERY tool, extension tools
|
|
6
|
+
// included (pi 1.4.0: createAgentSession({ tools }) leaves an extension tool
|
|
7
|
+
// that is not on the list inactive). So the argv must name each wizard tool,
|
|
8
|
+
// or the guide runs with no tools at all. The built-in belt stays read-only:
|
|
9
|
+
// bash, write and edit are never on the list.
|
|
10
|
+
|
|
11
|
+
/** pi's built-in tools the wizard may use: reading only. */
|
|
12
|
+
export const ONBOARD_BUILTIN_TOOLS: readonly string[] = ["read", "ls", "grep"];
|
|
13
|
+
|
|
14
|
+
/** The checklist tool. */
|
|
15
|
+
export const ONBOARD_STATUS_TOOL = "onboard_status";
|
|
16
|
+
/** The verify-everything-and-exit tool. */
|
|
17
|
+
export const ONBOARD_FINISH_TOOL = "finish_and_exit";
|
|
18
|
+
|
|
19
|
+
/** A step's tool name. */
|
|
20
|
+
export function onboardStepToolName(stepId: string): string {
|
|
21
|
+
return `onboard_${stepId}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Every tool the onboarding extension registers over these step ids, in
|
|
25
|
+
* registration order. */
|
|
26
|
+
export function onboardingExtensionToolNames(stepIds: readonly string[]): string[] {
|
|
27
|
+
return [...stepIds.map(onboardStepToolName), ONBOARD_STATUS_TOOL, ONBOARD_FINISH_TOOL];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The whole `--tools` allowlist for the onboarding pi: the read-only belt,
|
|
31
|
+
* then every wizard tool. */
|
|
32
|
+
export function onboardingToolAllowlist(stepIds: readonly string[]): string[] {
|
|
33
|
+
return [...ONBOARD_BUILTIN_TOOLS, ...onboardingExtensionToolNames(stepIds)];
|
|
34
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The one in-container shell that writes an operator command file as root
|
|
2
|
+
// into the root-sealed `<commands>/operator/` — its full rationale is on
|
|
3
|
+
// `operatorCommandWriteScript` in src/tui/unwedge.ts, the TUI's caller. A
|
|
4
|
+
// leaf module (no imports) so the onboarding wizard's proof round, which knows
|
|
5
|
+
// the release image's command root rather than a TUI config, uses the same
|
|
6
|
+
// script without reaching the TUI's module graph.
|
|
7
|
+
|
|
8
|
+
/** `operatorCommandWriteScript` for a known command root (`<root>/commands`
|
|
9
|
+
* inside the container) — the onboarding wizard's caller, which knows the
|
|
10
|
+
* release image's layout rather than a TUI config. */
|
|
11
|
+
export function operatorCommandWriteScriptAt(root: string, name: string): string {
|
|
12
|
+
const dir = `${root}/operator`;
|
|
13
|
+
const tmp = `.${name}.tmp`;
|
|
14
|
+
return (
|
|
15
|
+
`[ -d '${root}' ] || { echo 'daemon commands dir ${root} missing - is the daemon running?' >&2; exit 1; }; ` +
|
|
16
|
+
`mkdir -p '${dir}' || exit 1; ` +
|
|
17
|
+
`[ ! -L '${dir}' ] || { echo '${dir} is a symlink - refusing to write the operator command' >&2; exit 1; }; ` +
|
|
18
|
+
`R=$(cd -P '${root}' && pwd -P) || exit 1; ` +
|
|
19
|
+
`cd -P '${dir}' || exit 1; ` +
|
|
20
|
+
`[ "$(pwd -P)" = "$R/operator" ] || { echo '${dir} was replaced while writing - refusing to write the operator command' >&2; exit 1; }; ` +
|
|
21
|
+
`chown -h 0:0 . && chmod 0755 . || exit 1; ` +
|
|
22
|
+
`find . -maxdepth 1 -name '*.json' -mmin +1440 -delete 2>/dev/null; ` +
|
|
23
|
+
`rm -f '${tmp}' '${name}' && cat > '${tmp}' && chmod 0644 '${tmp}' && mv '${tmp}' '${name}'`
|
|
24
|
+
);
|
|
25
|
+
}
|
package/src/outcomes.ts
CHANGED
|
@@ -97,6 +97,7 @@ import { prActivity, type PrActivity } from "./pr-activity.js";
|
|
|
97
97
|
// read-only and memoised; a commit the clone does not hold is UNKNOWN, never a
|
|
98
98
|
// change and never no-change. Every failure is the sweep's own — reconcile's
|
|
99
99
|
// health (which lifts a deploy drain) never waits on it.
|
|
100
|
+
import { carriesOwnMarker } from "@quickchatai/pi-quickreview/src/identity.js";
|
|
100
101
|
import {
|
|
101
102
|
carriedToRef,
|
|
102
103
|
isPostedRow,
|
|
@@ -519,6 +520,10 @@ export interface OutcomeCaptureDeps {
|
|
|
519
520
|
store: StateStore;
|
|
520
521
|
/** `[github] bot_login` — whose root comments are findings. */
|
|
521
522
|
botLogin: string;
|
|
523
|
+
/** `[github] auth = "user"`: the login is the person's, so a root comment is
|
|
524
|
+
* a finding only when it carries the reviewer's marker, and the person's
|
|
525
|
+
* own replies are a human's. */
|
|
526
|
+
personMode?: boolean;
|
|
522
527
|
/** Absent ⇒ every "did the code change" question is unknown. */
|
|
523
528
|
git?: AnchorGit;
|
|
524
529
|
now?: () => Date;
|
|
@@ -585,6 +590,7 @@ export class OutcomeCapture {
|
|
|
585
590
|
this.store.db.transaction(() => {
|
|
586
591
|
for (const c of comments) {
|
|
587
592
|
if (c.user !== this.deps.botLogin || c.in_reply_to_id !== null) continue;
|
|
593
|
+
if (this.deps.personMode && !carriesOwnMarker(c.body)) continue;
|
|
588
594
|
const marker = parseFindingMarker(c.body);
|
|
589
595
|
const head = commentHead(c.body);
|
|
590
596
|
const info = stmt.run({
|
|
@@ -823,7 +829,8 @@ export class OutcomeCapture {
|
|
|
823
829
|
const replies = new Map<number, PrCommentRow[]>();
|
|
824
830
|
for (const c of this.store.prComments(pr)) {
|
|
825
831
|
if (c.source !== "review_comment" || c.in_reply_to_id === null) continue;
|
|
826
|
-
if (c.author === this.deps.botLogin)
|
|
832
|
+
if (c.author === this.deps.botLogin && (!this.deps.personMode || carriesOwnMarker(c.body)))
|
|
833
|
+
continue;
|
|
827
834
|
replies.set(c.in_reply_to_id, [...(replies.get(c.in_reply_to_id) ?? []), c]);
|
|
828
835
|
}
|
|
829
836
|
const events = this.store.prEvents(pr, [
|
package/src/posting-journal.ts
CHANGED
|
@@ -106,6 +106,10 @@ export interface JournalOptions {
|
|
|
106
106
|
/** `<state>/posting-receipts`, the dir the verdict watcher and the outbox read. */
|
|
107
107
|
dir: string;
|
|
108
108
|
repo: string;
|
|
109
|
+
/** `[github] auth = "user"`: every receipt says `person_mode`, the posting
|
|
110
|
+
* guard posts every event as a COMMENT, and an APPROVE posted as a COMMENT
|
|
111
|
+
* is NOT the withheld-approval shape (posting-receipt.ts). */
|
|
112
|
+
personMode?: boolean;
|
|
109
113
|
now?: () => Date;
|
|
110
114
|
}
|
|
111
115
|
|
|
@@ -357,7 +361,7 @@ export class PostingJournal {
|
|
|
357
361
|
// An APPROVE that GitHub holds as a COMMENT was softened by guard 4 on
|
|
358
362
|
// the gate after the CI preflight: the withheld-approval shape, which
|
|
359
363
|
// the outbox's confirmation stamps the same way.
|
|
360
|
-
...(o.intended === "APPROVE" && actual === "COMMENT"
|
|
364
|
+
...(o.intended === "APPROVE" && actual === "COMMENT" && !this.o.personMode
|
|
361
365
|
? { delivered_by: "daemon_retry" as const }
|
|
362
366
|
: {}),
|
|
363
367
|
}),
|
|
@@ -519,6 +523,7 @@ export class PostingJournal {
|
|
|
519
523
|
actual_event: actual,
|
|
520
524
|
payload_sha256: payloadHash(payload),
|
|
521
525
|
prepared_at: base.prepared_at ?? this.iso(),
|
|
526
|
+
...(this.o.personMode ? { person_mode: true } : {}),
|
|
522
527
|
...extra,
|
|
523
528
|
};
|
|
524
529
|
}
|
package/src/posting-outbox.ts
CHANGED
|
@@ -285,6 +285,10 @@ export class PostingOutbox {
|
|
|
285
285
|
repo: string;
|
|
286
286
|
dryRun: boolean;
|
|
287
287
|
botLogins: string[];
|
|
288
|
+
/** `[github] auth = "user"`: receipts say `person_mode`, and an APPROVE
|
|
289
|
+
* delivered as a COMMENT is the person-mode rule, not a guard-4
|
|
290
|
+
* softening (posting-receipt.ts). */
|
|
291
|
+
personMode?: boolean;
|
|
288
292
|
adapter: PostingDeliveryAdapter;
|
|
289
293
|
current: (job: PostingJob) => boolean;
|
|
290
294
|
now?: () => Date;
|
|
@@ -432,10 +436,13 @@ export class PostingOutbox {
|
|
|
432
436
|
gh_state: state,
|
|
433
437
|
// An APPROVE that GitHub holds as a COMMENT was softened by guard 4 on
|
|
434
438
|
// the gate, which only the daemon's delivery passes through (the shell
|
|
435
|
-
// route refuses a red-CI approval): the withheld-approval shape.
|
|
436
|
-
|
|
439
|
+
// route refuses a red-CI approval): the withheld-approval shape. Not in
|
|
440
|
+
// person mode, where every review is a COMMENT by rule.
|
|
441
|
+
...(deliveredBy ||
|
|
442
|
+
(j.intended_event === "APPROVE" && event === "COMMENT" && !this.deps.personMode)
|
|
437
443
|
? { delivered_by: "daemon_retry" as const }
|
|
438
444
|
: {}),
|
|
445
|
+
...(this.deps.personMode ? { person_mode: true as const } : {}),
|
|
439
446
|
} satisfies PostingReceipt);
|
|
440
447
|
j.status = "done";
|
|
441
448
|
writePostingFile(path, j);
|
|
@@ -762,6 +769,7 @@ export class PostingOutbox {
|
|
|
762
769
|
status: "prepared",
|
|
763
770
|
payload_sha256: j.payload_sha256,
|
|
764
771
|
prepared_at: j.attempted_at,
|
|
772
|
+
...(this.deps.personMode ? { person_mode: true as const } : {}),
|
|
765
773
|
} satisfies PostingReceipt);
|
|
766
774
|
try {
|
|
767
775
|
const result = await this.deps.adapter.createReview(j.pr, j.payload);
|
package/src/posting-receipt.ts
CHANGED
|
@@ -52,6 +52,11 @@ export interface PostingReceipt {
|
|
|
52
52
|
* (APPROVE posted as a COMMENT, approval withheld for red CI) is something
|
|
53
53
|
* the verdict file cannot know: see `retrySoftenedApproval`. */
|
|
54
54
|
delivered_by?: "daemon_retry";
|
|
55
|
+
/** Written by a person-mode install (`[github] auth = "user"`): the posting
|
|
56
|
+
* guard posts EVERY review as a COMMENT there, so `actual_event` COMMENT
|
|
57
|
+
* proves nothing about the decision — `intended_event` carries it, and an
|
|
58
|
+
* APPROVE posted as a COMMENT is not a withheld approval. */
|
|
59
|
+
person_mode?: true;
|
|
55
60
|
/** In memory only, never written by the posting scripts: the
|
|
56
61
|
* daemon SYNTHESISED this receipt from a bot review carrying the round's
|
|
57
62
|
* marker, because the shell never persisted its own. `intended_event` is
|
|
@@ -209,6 +214,7 @@ export function parsePostingReceipt(raw: string): PostingOutcome | undefined {
|
|
|
209
214
|
value.status !== "deferred") ||
|
|
210
215
|
(value.status === "deferred" && !isoDate(value.deferred_at)) ||
|
|
211
216
|
(value.delivered_by !== undefined && value.delivered_by !== "daemon_retry") ||
|
|
217
|
+
(value.person_mode !== undefined && value.person_mode !== true) ||
|
|
212
218
|
typeof value.payload_sha256 !== "string" ||
|
|
213
219
|
!/^[a-f0-9]{64}$/.test(value.payload_sha256) ||
|
|
214
220
|
!isoDate(value.prepared_at)
|
|
@@ -245,6 +251,7 @@ export function parsePostingReceipt(raw: string): PostingOutcome | undefined {
|
|
|
245
251
|
* the receipt is the evidence that the approval was withheld. */
|
|
246
252
|
export function retrySoftenedApproval(receipt: PostingReceipt): boolean {
|
|
247
253
|
return (
|
|
254
|
+
receipt.person_mode !== true &&
|
|
248
255
|
receipt.delivered_by === "daemon_retry" &&
|
|
249
256
|
receipt.status === "confirmed" &&
|
|
250
257
|
receipt.intended_event === "APPROVE" &&
|
|
@@ -259,6 +266,16 @@ export function receiptMatchesVerdict(receipt: PostingReceipt, verdict: VerdictF
|
|
|
259
266
|
receipt.head_sha.toLowerCase() !== verdict.head_sha.toLowerCase()
|
|
260
267
|
)
|
|
261
268
|
return false;
|
|
269
|
+
// Person mode posts every review as a COMMENT. A receipt synthesised from
|
|
270
|
+
// GitHub knows nothing more than that; any other one is matched on what
|
|
271
|
+
// the session asked for, as if it had been posted as asked.
|
|
272
|
+
if (receipt.person_mode === true && receipt.actual_event === "COMMENT") {
|
|
273
|
+
if (receipt.reconciled === true) return true;
|
|
274
|
+
const asAsked: PostingReceipt = { ...receipt, actual_event: receipt.intended_event };
|
|
275
|
+
delete asAsked.person_mode;
|
|
276
|
+
delete asAsked.delivered_by;
|
|
277
|
+
return receiptMatchesVerdict(asAsked, verdict);
|
|
278
|
+
}
|
|
262
279
|
if (verdict.verdict === "REQUEST_CHANGES")
|
|
263
280
|
return receipt.actual_event === "REQUEST_CHANGES" || receipt.actual_event === "COMMENT";
|
|
264
281
|
if (verdict.verdict === "APPROVE" && retrySoftenedApproval(receipt)) return true;
|
package/src/recheck.ts
CHANGED
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
// 5. **No GitHub write lives here.** The module reads (compare, files) and
|
|
39
39
|
// writes nothing; the session posts its review through the gated scripts.
|
|
40
40
|
// The MutationGate keeps its monopoly by this module having no mutation.
|
|
41
|
+
import { botAccountLogin } from "@quickchatai/pi-quickreview/src/identity.js";
|
|
41
42
|
import { existsSync, mkdirSync, readdirSync, renameSync, watch, type FSWatcher } from "node:fs";
|
|
42
43
|
import { join } from "node:path";
|
|
43
44
|
import type { Config } from "./config.js";
|
|
@@ -1647,7 +1648,7 @@ export class RecheckWatcher {
|
|
|
1647
1648
|
{ type: "verdict", verdict: "REQUEST_CHANGES" },
|
|
1648
1649
|
{},
|
|
1649
1650
|
{
|
|
1650
|
-
botLogin: this.deps.cfg.github
|
|
1651
|
+
botLogin: botAccountLogin(this.deps.cfg.github),
|
|
1651
1652
|
...(this.deps.now ? { now: this.deps.now } : {}),
|
|
1652
1653
|
reason: "recheck:request-changes",
|
|
1653
1654
|
},
|
package/src/reconcile.ts
CHANGED
|
@@ -29,7 +29,12 @@ import { DEFAULT_ROUND_STALL_GRACE_MIN, type Config } from "./config.js";
|
|
|
29
29
|
import type { IssueCommentFull, PrRef, ReviewCommentFull, ReviewFull } from "./github.js";
|
|
30
30
|
import type { OutcomeCapture, OutcomeSweepReport } from "./outcomes.js";
|
|
31
31
|
import { applyEvent, isBotAuthor, mergedOverBlock } from "./statemachine.js";
|
|
32
|
-
import
|
|
32
|
+
import {
|
|
33
|
+
botAccountLogin,
|
|
34
|
+
isOwnContent,
|
|
35
|
+
isPersonMode,
|
|
36
|
+
} from "@quickchatai/pi-quickreview/src/identity.js";
|
|
37
|
+
import type { OpenPrSummary, RoundRow, StateStore } from "./state.js";
|
|
33
38
|
import type { OvSessionSweepReport, SessionHooks } from "@quickchatai/pi-quickreview/src/types.js";
|
|
34
39
|
import { performance } from "node:perf_hooks";
|
|
35
40
|
import { yieldToLoop } from "@quickchatai/pi-quickreview/src/yieldloop.js";
|
|
@@ -132,6 +137,9 @@ export const RECEIPT_SWEEP_BUDGET_MS = 20_000;
|
|
|
132
137
|
export interface ReconcileReport {
|
|
133
138
|
ran_at: string;
|
|
134
139
|
open_prs: number;
|
|
140
|
+
/** The open PRs this pass listed, tracked or not; absent when the list
|
|
141
|
+
* step failed. The daemon publishes it in status.json for the board. */
|
|
142
|
+
open_list?: OpenPrSummary[];
|
|
135
143
|
triggered: number[];
|
|
136
144
|
closed: number[];
|
|
137
145
|
backfilled_comments: number;
|
|
@@ -255,7 +263,11 @@ export function reconcileOrphanedRounds(
|
|
|
255
263
|
export async function reconcileOnce(deps: ReconcileDeps): Promise<ReconcileReport> {
|
|
256
264
|
const { store, cfg, adapter } = deps;
|
|
257
265
|
const now = deps.now ?? (() => new Date());
|
|
258
|
-
|
|
266
|
+
// The bot ACCOUNT: whose PRs are a bot's and whose captured comments are not
|
|
267
|
+
// a human's. "" in person mode (`[github] auth = "user"`), where the
|
|
268
|
+
// configured login is the person's: their PRs are reviewed, and their own
|
|
269
|
+
// comments are told from the reviewer's by its marker (identity.ts).
|
|
270
|
+
const botLogin = botAccountLogin(cfg.github);
|
|
259
271
|
const report: ReconcileReport = {
|
|
260
272
|
ran_at: now().toISOString(),
|
|
261
273
|
open_prs: 0,
|
|
@@ -287,6 +299,13 @@ export async function reconcileOnce(deps: ReconcileDeps): Promise<ReconcileRepor
|
|
|
287
299
|
for (const pr of await adapter.listOpenPrs()) open.set(pr.number, pr);
|
|
288
300
|
openListed = true;
|
|
289
301
|
report.open_prs = open.size;
|
|
302
|
+
report.open_list = [...open.values()].map((p) => ({
|
|
303
|
+
number: p.number,
|
|
304
|
+
title: p.title,
|
|
305
|
+
author: p.author,
|
|
306
|
+
draft: p.draft,
|
|
307
|
+
updated_at: p.updated_at,
|
|
308
|
+
}));
|
|
290
309
|
for (const pr of open.values()) {
|
|
291
310
|
if (isBotAuthor(pr.author, botLogin)) continue;
|
|
292
311
|
const local = store.getPr(pr.number);
|
|
@@ -921,6 +940,8 @@ export async function reconcileOnce(deps: ReconcileDeps): Promise<ReconcileRepor
|
|
|
921
940
|
}
|
|
922
941
|
for (const c of rows) {
|
|
923
942
|
if (!isHumanCapture(c, botLogin)) continue;
|
|
943
|
+
// Person mode: the reviewer's own words under the person's login.
|
|
944
|
+
if (isOwnContent(cfg.github, c.author, c.body)) continue;
|
|
924
945
|
if (recordCapturedComment(store, c, "reconcile")) report.captured_comments++;
|
|
925
946
|
}
|
|
926
947
|
// ---- the exec boundary's OUTCOME layer ------------------------------
|
|
@@ -947,7 +968,12 @@ export async function reconcileOnce(deps: ReconcileDeps): Promise<ReconcileRepor
|
|
|
947
968
|
reviewComments: commentsByReview(inlineFetched),
|
|
948
969
|
gatedCommentIds: store.botCommentIds(n),
|
|
949
970
|
},
|
|
950
|
-
{
|
|
971
|
+
{
|
|
972
|
+
botLogin: cfg.github.bot_login,
|
|
973
|
+
appSlug: cfg.github.app_slug,
|
|
974
|
+
// Person mode: only marker-bearing reviews are the reviewer's.
|
|
975
|
+
personMode: isPersonMode(cfg.github),
|
|
976
|
+
},
|
|
951
977
|
);
|
|
952
978
|
if (ungated.length > 0) {
|
|
953
979
|
store.logEvent("ungated_review_detected", n, {
|
package/src/reviewhistory.ts
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
// Total at the edge: a PR that cannot be read is counted, never fatal; an
|
|
19
19
|
// empty repo — no merged PRs, no rounds, no feedback — is an ordinary, EMPTY
|
|
20
20
|
// digest, not an error.
|
|
21
|
+
import {
|
|
22
|
+
excludeOwnAppQualifier,
|
|
23
|
+
isOwnContent,
|
|
24
|
+
isPersonMode,
|
|
25
|
+
} from "@quickchatai/pi-quickreview/src/identity.js";
|
|
21
26
|
import type { Config } from "./config.js";
|
|
22
27
|
import type { SearchIssueItem } from "./github.js";
|
|
23
28
|
import type { FeedbackSignalRow, RoundRow } from "./state.js";
|
|
@@ -146,7 +151,7 @@ export function mergedPrsQuery(cfg: Config, since: string, until?: string): stri
|
|
|
146
151
|
until === undefined
|
|
147
152
|
? `>=${since.slice(0, 10)}`
|
|
148
153
|
: `${since.slice(0, 10)}..${until.slice(0, 10)}`;
|
|
149
|
-
return `repo:${cfg.github.repo} is:pr is:merged merged:${range}
|
|
154
|
+
return `repo:${cfg.github.repo} is:pr is:merged merged:${range}${excludeOwnAppQualifier(cfg.github)}`;
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
/**
|
|
@@ -173,11 +178,20 @@ function clip(body: string): string {
|
|
|
173
178
|
}
|
|
174
179
|
|
|
175
180
|
/** A human, and not the PR's own author (their replies answer a review; they
|
|
176
|
-
* are not one) nor this App under either spelling.
|
|
177
|
-
*
|
|
178
|
-
|
|
181
|
+
* are not one) nor this App under either spelling. In person mode
|
|
182
|
+
* (`[github] auth = "user"`) the login is the person's: their words are a
|
|
183
|
+
* human's, and only the reviewer's marker-bearing ones are left out.
|
|
184
|
+
* `[feedback]` comments are left out here: they are the feedback half, read
|
|
185
|
+
* from the ledger. */
|
|
186
|
+
function humanReviewer(
|
|
187
|
+
a: HumanReviewAuthor | null,
|
|
188
|
+
pr: PrReviews,
|
|
189
|
+
cfg: Config,
|
|
190
|
+
body: string,
|
|
191
|
+
): string | null {
|
|
179
192
|
if (!a || a.type !== "User" || a.login === "") return null;
|
|
180
193
|
if (a.login === pr.author) return null;
|
|
194
|
+
if (isPersonMode(cfg.github)) return isOwnContent(cfg.github, a.login, body) ? null : a.login;
|
|
181
195
|
if (a.login === cfg.backfill.bot_login || a.login === cfg.backfill.bot_actor) return null;
|
|
182
196
|
return a.login;
|
|
183
197
|
}
|
|
@@ -187,7 +201,7 @@ export function humanCommentsOf(pr: PrReviews, cfg: Config): HumanComment[] {
|
|
|
187
201
|
const out: HumanComment[] = [];
|
|
188
202
|
const keep = (body: string): boolean => body.trim() !== "" && !/^\s*\[feedback\]/i.test(body);
|
|
189
203
|
for (const r of pr.reviews) {
|
|
190
|
-
const reviewer = humanReviewer(r.author, pr, cfg);
|
|
204
|
+
const reviewer = humanReviewer(r.author, pr, cfg, r.body);
|
|
191
205
|
if (reviewer !== null && keep(r.body)) {
|
|
192
206
|
out.push({
|
|
193
207
|
pr: pr.number,
|
|
@@ -200,7 +214,7 @@ export function humanCommentsOf(pr: PrReviews, cfg: Config): HumanComment[] {
|
|
|
200
214
|
});
|
|
201
215
|
}
|
|
202
216
|
for (const c of r.comments) {
|
|
203
|
-
const who = humanReviewer(c.author, pr, cfg);
|
|
217
|
+
const who = humanReviewer(c.author, pr, cfg, c.body);
|
|
204
218
|
if (who === null || !keep(c.body)) continue;
|
|
205
219
|
out.push({
|
|
206
220
|
pr: pr.number,
|