@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
package/src/cli.ts
CHANGED
|
@@ -68,6 +68,7 @@ import { sanitizeText } from "./tui/paint.js";
|
|
|
68
68
|
import { operatorCommandWriteScript } from "./tui/unwedge.js";
|
|
69
69
|
import { shellQuote, transportOf } from "./tui/remote.js";
|
|
70
70
|
import { MutationGate } from "@quickchatai/pi-quickreview/src/gate.js";
|
|
71
|
+
import { botAccountLogin, isPersonMode } from "@quickchatai/pi-quickreview/src/identity.js";
|
|
71
72
|
import type { GitHubAdapter } from "./github.js";
|
|
72
73
|
import type { OutcomeCapture } from "./outcomes.js";
|
|
73
74
|
import type { Learner, MineReport } from "./learner/learner.js";
|
|
@@ -198,17 +199,18 @@ export async function cmdOutcomes(rest: string[], json: boolean): Promise<number
|
|
|
198
199
|
const outcomes = new OutcomeCapture({
|
|
199
200
|
store,
|
|
200
201
|
botLogin: cfg.github.bot_login,
|
|
202
|
+
personMode: isPersonMode(cfg.github),
|
|
201
203
|
...(existsSync(worktrees.basePath) ? { git: worktrees } : {}),
|
|
202
204
|
});
|
|
203
205
|
const since = new Date(Date.now() - days * 86_400_000).toISOString();
|
|
204
206
|
if (capture) {
|
|
205
207
|
let read: GitHubAdapter | undefined;
|
|
206
208
|
if (readGithub) {
|
|
207
|
-
const {
|
|
209
|
+
const { githubCredential } = await import("@quickchatai/pi-quickreview/src/githubAuth.js");
|
|
208
210
|
const { GitHubAdapter } = await import("./github.js");
|
|
209
211
|
read = new GitHubAdapter(
|
|
210
212
|
cfg,
|
|
211
|
-
|
|
213
|
+
githubCredential(cfg.github),
|
|
212
214
|
recordOnlyGate(join(stateDir(), "dryrun")),
|
|
213
215
|
);
|
|
214
216
|
}
|
|
@@ -365,7 +367,8 @@ export async function cmdLessons(rest: string[], json: boolean): Promise<number>
|
|
|
365
367
|
root,
|
|
366
368
|
eodTime: cfg.eod.time,
|
|
367
369
|
timeZone: cfg.eod.timezone,
|
|
368
|
-
|
|
370
|
+
// The bot ACCOUNT ("" in person mode, where the person's words are a human's).
|
|
371
|
+
botLogin: botAccountLogin(cfg.github),
|
|
369
372
|
baseBranch: cfg.github.base_branch,
|
|
370
373
|
config: learnerKnobsOf(cfg.memory),
|
|
371
374
|
...(existsSync(worktrees.basePath) ? { git: worktrees } : {}),
|
|
@@ -703,13 +706,13 @@ export interface DoubtedFinding {
|
|
|
703
706
|
* when the developer machine carries the app credential. */
|
|
704
707
|
export async function enrichWhy(payload: WhyPayload, secretsDir?: string): Promise<WhyPayload> {
|
|
705
708
|
try {
|
|
706
|
-
const {
|
|
709
|
+
const { githubCredential } = await import("@quickchatai/pi-quickreview/src/githubAuth.js");
|
|
707
710
|
const { GitHubAdapter } = await import("./github.js");
|
|
708
711
|
const cfg = loadConfig();
|
|
709
712
|
const gate = recordOnlyGate(join(stateDir(), "dryrun"), "qcr why");
|
|
710
713
|
const adapter = new GitHubAdapter(
|
|
711
714
|
cfg,
|
|
712
|
-
|
|
715
|
+
githubCredential({ ...cfg.github, secrets_dir: secretsDir ?? cfg.github.secrets_dir }),
|
|
713
716
|
gate,
|
|
714
717
|
);
|
|
715
718
|
const convo = await adapter.prConversation(payload.pr, 3);
|
|
@@ -928,14 +931,22 @@ export async function cmdBackfill(
|
|
|
928
931
|
limit: number | undefined,
|
|
929
932
|
json: boolean,
|
|
930
933
|
): Promise<void> {
|
|
931
|
-
const {
|
|
934
|
+
const { githubCredential } = await import("@quickchatai/pi-quickreview/src/githubAuth.js");
|
|
932
935
|
const { GitHubAdapter } = await import("./github.js");
|
|
933
936
|
const { runBackfill } = await import("./backfill.js");
|
|
934
937
|
const cfg = loadConfig();
|
|
938
|
+
// The import reads "reviews by the bot's login" as the bot's history. Under
|
|
939
|
+
// the person's own credential that login is the person's, and their own
|
|
940
|
+
// reviews are not the bot's: there is nothing this importer can tell apart.
|
|
941
|
+
if (isPersonMode(cfg.github))
|
|
942
|
+
throw new Error(
|
|
943
|
+
'qcr backfill imports a GitHub App\'s review history; under [github] auth = "user" the login is ' +
|
|
944
|
+
"your own, so your reviews and the bot's cannot be told apart by it — nothing to import",
|
|
945
|
+
);
|
|
935
946
|
// A gate that can only ever record: the importer performs no mutations, and
|
|
936
947
|
// this keeps that structural rather than prompted.
|
|
937
948
|
const gate = recordOnlyGate(join(stateDir(), "dryrun"), "qcr backfill");
|
|
938
|
-
const adapter = new GitHubAdapter(cfg,
|
|
949
|
+
const adapter = new GitHubAdapter(cfg, githubCredential(cfg.github), gate);
|
|
939
950
|
const store = openStore();
|
|
940
951
|
try {
|
|
941
952
|
const stats = await runBackfill(store, adapter, cfg, {
|
|
@@ -976,12 +987,12 @@ export async function cmdHistory(rest: string[]): Promise<number> {
|
|
|
976
987
|
const months = intFlag(rest, "--months", HISTORY_USAGE);
|
|
977
988
|
if (sub === "digest") {
|
|
978
989
|
const limit = intFlag(rest, "--limit", HISTORY_USAGE);
|
|
979
|
-
const {
|
|
990
|
+
const { githubCredential } = await import("@quickchatai/pi-quickreview/src/githubAuth.js");
|
|
980
991
|
const { GitHubAdapter } = await import("./github.js");
|
|
981
992
|
const { collectReviewHistory } = await import("./reviewhistory.js");
|
|
982
993
|
const cfg = loadConfig();
|
|
983
994
|
const gate = recordOnlyGate(join(stateDir(), "dryrun"), "qcr history");
|
|
984
|
-
const adapter = new GitHubAdapter(cfg,
|
|
995
|
+
const adapter = new GitHubAdapter(cfg, githubCredential(cfg.github), gate);
|
|
985
996
|
const store = openStore();
|
|
986
997
|
try {
|
|
987
998
|
const digest = await collectReviewHistory(adapter, store, cfg, {
|
|
@@ -1230,12 +1241,13 @@ async function cmdDrain(rest: string[]): Promise<number> {
|
|
|
1230
1241
|
// until `qcr resume-pr`, and no session lifts it. The cutover wizard's stage 9
|
|
1231
1242
|
// pauses every tracked PR but one with it before the drain is lifted.
|
|
1232
1243
|
|
|
1233
|
-
export const PR_LEVER_USAGE =
|
|
1244
|
+
export const PR_LEVER_USAGE =
|
|
1245
|
+
"usage: qcr pause-pr <pr> [--reason <text>] | qcr resume-pr <pr> | qcr review-now <pr>";
|
|
1234
1246
|
|
|
1235
1247
|
/** `qcr pause-pr|resume-pr`'s arguments: one PR number, and for a pause an
|
|
1236
1248
|
* optional `--reason <text>` (the reason the board and `qcr status` show). */
|
|
1237
1249
|
export function parsePrLeverArgs(
|
|
1238
|
-
verb: "pause-pr" | "resume-pr",
|
|
1250
|
+
verb: "pause-pr" | "resume-pr" | "review-now",
|
|
1239
1251
|
rest: readonly string[],
|
|
1240
1252
|
): import("./tui/prpause.js").PrLeverCommand {
|
|
1241
1253
|
let pr: number | undefined;
|
|
@@ -1251,10 +1263,14 @@ export function parsePrLeverArgs(
|
|
|
1251
1263
|
}
|
|
1252
1264
|
if (pr === undefined) throw new Error(PR_LEVER_USAGE);
|
|
1253
1265
|
if (verb === "resume-pr") return { command: "resume-pr", pr };
|
|
1266
|
+
if (verb === "review-now") return { command: "review-now", pr };
|
|
1254
1267
|
return { command: "pause-pr", pr, reason: reason ?? "paused by the operator (qcr pause-pr)" };
|
|
1255
1268
|
}
|
|
1256
1269
|
|
|
1257
|
-
async function cmdPrLever(
|
|
1270
|
+
async function cmdPrLever(
|
|
1271
|
+
verb: "pause-pr" | "resume-pr" | "review-now",
|
|
1272
|
+
rest: string[],
|
|
1273
|
+
): Promise<number> {
|
|
1258
1274
|
const cmd = parsePrLeverArgs(verb, rest);
|
|
1259
1275
|
const cfg = loadConfig();
|
|
1260
1276
|
const { runnerFor } = await import("./tui/remote.js");
|
|
@@ -1540,6 +1556,7 @@ export async function cmdCompanion(sub: string | undefined): Promise<void> {
|
|
|
1540
1556
|
dir: companionGithubDir(),
|
|
1541
1557
|
repo: full.github.repo,
|
|
1542
1558
|
secretsDir: full.github.secrets_dir,
|
|
1559
|
+
auth: full.github.auth ?? "app",
|
|
1543
1560
|
}).refresh();
|
|
1544
1561
|
console.log(`companion ${cfg.container_name}: ${tokenOutcomeLine(gh)}`);
|
|
1545
1562
|
const mirror = lc.syncConfigMirror();
|
|
@@ -2157,10 +2174,28 @@ export async function main(rawArgv: string[]): Promise<void> {
|
|
|
2157
2174
|
return;
|
|
2158
2175
|
}
|
|
2159
2176
|
case "pause-pr":
|
|
2160
|
-
case "resume-pr":
|
|
2177
|
+
case "resume-pr":
|
|
2178
|
+
case "review-now": {
|
|
2179
|
+
// review-now: the operator's "review this PR now" — queued as a
|
|
2180
|
+
// human-asked round through the sealed operator channel, no label.
|
|
2161
2181
|
process.exitCode = await cmdPrLever(cmd, rest);
|
|
2162
2182
|
return;
|
|
2163
2183
|
}
|
|
2184
|
+
case "github-token": {
|
|
2185
|
+
// The own-account route's token refresher (src/githubtoken.ts): gh's
|
|
2186
|
+
// token → <secrets>/github_token, 0600. The crontab entry runs this.
|
|
2187
|
+
const { cmdGithubToken } = await import("./githubtoken.js");
|
|
2188
|
+
process.exitCode = await cmdGithubToken(rest, {
|
|
2189
|
+
secretsDir: () => {
|
|
2190
|
+
try {
|
|
2191
|
+
return loadConfig().github.secrets_dir;
|
|
2192
|
+
} catch {
|
|
2193
|
+
return locateSecrets({}).path;
|
|
2194
|
+
}
|
|
2195
|
+
},
|
|
2196
|
+
});
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2164
2199
|
case "message": {
|
|
2165
2200
|
process.exitCode = await cmdMessage(rest);
|
|
2166
2201
|
return;
|
|
@@ -2238,7 +2273,7 @@ export async function main(rawArgv: string[]): Promise<void> {
|
|
|
2238
2273
|
}
|
|
2239
2274
|
default:
|
|
2240
2275
|
console.log(
|
|
2241
|
-
"usage: qcr <version|config check [--config-home <dir>]|onboard|setup [validate [<profile>]|commit]|status|spend|today|why <n>|metrics [--pr <n>]|events|takeover <n>|debug-round <n>|release <n>|tui [pr|overseer]|drain <on [--hold]|off>|pause-pr <pr> [--reason <text>]|resume-pr <pr>|message <pr> [--may-post] <text...>|archive <status [--plan]|run|restore <pr>|extract <pr> --to <dir>>|install <where|migrate>|" +
|
|
2276
|
+
"usage: qcr <version|config check [--config-home <dir>]|onboard|setup [validate [<profile>]|commit]|status|spend|today|why <n>|metrics [--pr <n>]|events|takeover <n>|debug-round <n>|release <n>|tui [pr|overseer]|drain <on [--hold]|off>|pause-pr <pr> [--reason <text>]|resume-pr <pr>|review-now <pr>|github-token sync [--gh <path>] [--secrets-dir <dir>]|message <pr> [--may-post] <text...>|archive <status [--plan]|run|restore <pr>|extract <pr> --to <dir>>|install <where|migrate>|" +
|
|
2242
2277
|
"companion <up|down|status|rebuild>|review [start|status|stop|branch|--uncommitted|--diff FILE|-|<PR number|URL>] [--repo OWNER/NAME] [--base REF] [--preview] [--key K] [--limit USD] [--json]|live [RUN_ID|DIR] [--lens ID] [--round N] [--follow] [--json]|open [RUN_ID|DIR|FILE.jsonl|scan[:RUN]|pr N [--round R]] [--lens ID] [--write] [--print]|scan <preview|start|status|stop|findings|matches|match|suggestions|stage2 propose|stage2 start|dismiss|units show|units propose|units adjust|units save|login|doctor|rebuild>|memory reembed [--user <name>]|memory relayout [--plan|--apply]|backfill [--dry-run] [--limit <n>]|history <digest|pull>|outcomes [--days <n>] [--capture [--read-github]]|lessons [mine|shadow-report [--days <n>]|off|on|suppress <id>|unsuppress <id>]|checklist|compare <pr...>|" +
|
|
2243
2278
|
"regression [--set <path>] [--clone <path>] [--change <name>] [--limit <n>] [--out <path>]>",
|
|
2244
2279
|
);
|
package/src/cliops.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// qcr checklist / qcr compare — production wiring of the cutover harnesses. Local
|
|
2
2
|
// runs exercise the offline halves; the live halves (tunnel, real searches,
|
|
3
3
|
// live rounds) light up on the VM where the daemon and pi are running.
|
|
4
|
+
import { botAccountLogin } from "@quickchatai/pi-quickreview/src/identity.js";
|
|
4
5
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
5
6
|
import { join } from "node:path";
|
|
6
7
|
import { runChecklist, type ChecklistDeps } from "./checklist.js";
|
|
@@ -253,7 +254,7 @@ export async function cmdChecklist(parts?: ProductionParts): Promise<number> {
|
|
|
253
254
|
await p.adapter.searchLabelTriggered(),
|
|
254
255
|
"label",
|
|
255
256
|
cfg.dry_run,
|
|
256
|
-
cfg.github
|
|
257
|
+
botAccountLogin(cfg.github),
|
|
257
258
|
store,
|
|
258
259
|
),
|
|
259
260
|
teamTriggered: async () =>
|
|
@@ -261,7 +262,7 @@ export async function cmdChecklist(parts?: ProductionParts): Promise<number> {
|
|
|
261
262
|
await p.adapter.searchTeamTriggered(),
|
|
262
263
|
"team_request",
|
|
263
264
|
cfg.dry_run,
|
|
264
|
-
cfg.github
|
|
265
|
+
botAccountLogin(cfg.github),
|
|
265
266
|
store,
|
|
266
267
|
),
|
|
267
268
|
derivedQueue: () => derivedQueueOf(store.allPrs(), cfg.dry_run),
|
package/src/commands.ts
CHANGED
|
@@ -99,7 +99,10 @@ export const CONTROL_TEXT_MAX = 8000;
|
|
|
99
99
|
* session's command file cannot spell them: in the session directories, and
|
|
100
100
|
* in an UNSEALED operator directory (served as a session's), they are refused
|
|
101
101
|
* at validation, and `executeCommand` refuses
|
|
102
|
-
* them again for any issuer but the operator.
|
|
102
|
+
* them again for any issuer but the operator. `review-now` is the operator's
|
|
103
|
+
* "review this PR now": one PR queued as a human-asked round, exactly as a
|
|
104
|
+
* fresh trigger label would queue it, with no GitHub write to ask for it (the
|
|
105
|
+
* onboarding wizard's proof round sends it). The kill switch's state lives
|
|
103
106
|
* in state.db (src/lessonledger.ts), never as a memory tag anyone holding a
|
|
104
107
|
* key could write.
|
|
105
108
|
*/
|
|
@@ -110,6 +113,7 @@ export const OPERATOR_COMMANDS = [
|
|
|
110
113
|
"unsuppress-lesson",
|
|
111
114
|
"archive-run",
|
|
112
115
|
"archive-restore",
|
|
116
|
+
"review-now",
|
|
113
117
|
...CONTROL_COMMANDS,
|
|
114
118
|
] as const;
|
|
115
119
|
|
|
@@ -180,6 +184,9 @@ export type Command =
|
|
|
180
184
|
* valid in a session's file. */
|
|
181
185
|
| { command: "archive-run" }
|
|
182
186
|
| { command: "archive-restore"; pr: number }
|
|
187
|
+
/** The operator's "review this PR now" (`OPERATOR_COMMANDS`): never valid
|
|
188
|
+
* in a session's file. */
|
|
189
|
+
| { command: "review-now"; pr: number }
|
|
183
190
|
/** The operator's round controls (`CONTROL_COMMANDS`): never valid in a
|
|
184
191
|
* session's file. */
|
|
185
192
|
| { command: "steer"; pr: number; text: string }
|
|
@@ -263,6 +270,9 @@ export interface CommandDeps {
|
|
|
263
270
|
* only for an operator-issued command; optional like `drain`. */
|
|
264
271
|
archiveRun?(file: string): Promise<FileOutcome | void> | FileOutcome | void;
|
|
265
272
|
archiveRestore?(pr: number, file: string): Promise<FileOutcome | void> | FileOutcome | void;
|
|
273
|
+
/** The operator's "review this PR now": queue one PR for a human-asked
|
|
274
|
+
* round. Reached only for an operator-issued command; optional like `drain`. */
|
|
275
|
+
reviewNow?(pr: number, file: string): Promise<FileOutcome | void> | FileOutcome | void;
|
|
266
276
|
/** The operator's round controls (`CONTROL_COMMANDS`), forwarded to the PR's
|
|
267
277
|
* host. Reached only for an operator-issued command; optional like `drain`,
|
|
268
278
|
* so a daemon with no worker host refuses them with a reason. */
|
|
@@ -338,7 +348,7 @@ export function validateCommand(raw: unknown, opts: ValidateOptions = {}): Valid
|
|
|
338
348
|
ok: false,
|
|
339
349
|
reason:
|
|
340
350
|
`command '${name}' is the OPERATOR's alone: it is accepted only from the root-sealed commands/operator/ ` +
|
|
341
|
-
"channel (`qcr lessons off|on|suppress|unsuppress`, `qcr archive run|restore`), never from a session's command file",
|
|
351
|
+
"channel (`qcr lessons off|on|suppress|unsuppress`, `qcr archive run|restore`, `qcr review-now`), never from a session's command file",
|
|
342
352
|
};
|
|
343
353
|
return validateOperatorCommand(name, obj);
|
|
344
354
|
}
|
|
@@ -609,6 +619,12 @@ function validateOperatorCommand(
|
|
|
609
619
|
if (!isPrNumber(obj["pr"])) return { ok: false, reason: "'pr' must be a positive integer" };
|
|
610
620
|
return { ok: true, command: { command: "archive-restore", pr: obj["pr"] } };
|
|
611
621
|
}
|
|
622
|
+
case "review-now": {
|
|
623
|
+
const err = fieldError(obj, ["pr"]);
|
|
624
|
+
if (err) return { ok: false, reason: err };
|
|
625
|
+
if (!isPrNumber(obj["pr"])) return { ok: false, reason: "'pr' must be a positive integer" };
|
|
626
|
+
return { ok: true, command: { command: "review-now", pr: obj["pr"] } };
|
|
627
|
+
}
|
|
612
628
|
case "steer":
|
|
613
629
|
case "follow-up":
|
|
614
630
|
case "message": {
|
|
@@ -971,6 +987,16 @@ export async function executeCommand(
|
|
|
971
987
|
return { ok: false, reason: "archive-restore is not wired on this daemon" };
|
|
972
988
|
return normalize(await deps.archiveRestore(command.pr, ctx.delivery_key ?? ctx.file));
|
|
973
989
|
}
|
|
990
|
+
case "review-now": {
|
|
991
|
+
// The belt behind validation's brace, as for the kill switch.
|
|
992
|
+
if (ctx.issuer !== "operator")
|
|
993
|
+
return {
|
|
994
|
+
ok: false,
|
|
995
|
+
reason: `'${command.command}' is the operator's alone — refused for issuer '${ctx.issuer}'`,
|
|
996
|
+
};
|
|
997
|
+
if (!deps.reviewNow) return { ok: false, reason: "review-now is not wired on this daemon" };
|
|
998
|
+
return normalize(await deps.reviewNow(command.pr, ctx.delivery_key ?? ctx.file));
|
|
999
|
+
}
|
|
974
1000
|
case "steer":
|
|
975
1001
|
case "follow-up":
|
|
976
1002
|
case "abort-round":
|
package/src/config.ts
CHANGED
|
@@ -974,8 +974,11 @@ function checkSectionTimeZone<T extends { timezone: string }>(section: T, where:
|
|
|
974
974
|
* what an absent section means) starts nothing at all.
|
|
975
975
|
*/
|
|
976
976
|
export interface IngressConfig {
|
|
977
|
-
/** "none" ⇒ no watcher is ever constructed; "tunnel-file" ⇒ self-heal on
|
|
978
|
-
|
|
977
|
+
/** "none" ⇒ no watcher is ever constructed; "tunnel-file" ⇒ self-heal on;
|
|
978
|
+
* "polling" ⇒ no webhooks at all: the daemon's `/webhook` route refuses
|
|
979
|
+
* every delivery, no webhook secret is read, and the reconcile poll is the
|
|
980
|
+
* only trigger. `[github] auth = "user"` requires "polling". */
|
|
981
|
+
shape: "none" | "tunnel-file" | "polling";
|
|
979
982
|
/** File the tunnel sidecar writes the bare public hostname into. Empty ⇒
|
|
980
983
|
* `<root>/state/ingress/hostname` (see ingressHostnameFile). */
|
|
981
984
|
hostname_file: string;
|
|
@@ -994,6 +997,13 @@ export const INGRESS_DEFAULTS: IngressConfig = {
|
|
|
994
997
|
sweep_interval_s: 300,
|
|
995
998
|
};
|
|
996
999
|
|
|
1000
|
+
/** Does this install take webhooks at all? Not under the polling shape
|
|
1001
|
+
* (and so never under `[github] auth = "user"`, which requires it): there the
|
|
1002
|
+
* `/webhook` route refuses every delivery and no webhook secret is read. */
|
|
1003
|
+
export function webhooksEnabled(cfg: Pick<Config, "ingress">): boolean {
|
|
1004
|
+
return cfg.ingress.shape !== "polling";
|
|
1005
|
+
}
|
|
1006
|
+
|
|
997
1007
|
/** The hostname file's resolved path — the one place the default is spelled. */
|
|
998
1008
|
export function ingressHostnameFile(cfg: Config, root: string): string {
|
|
999
1009
|
const configured = cfg.ingress.hostname_file.trim();
|
|
@@ -1729,9 +1739,19 @@ export function parseConfig(toml: string): Config {
|
|
|
1729
1739
|
...INGRESS_DEFAULTS,
|
|
1730
1740
|
...((raw.ingress ?? {}) as Partial<IngressConfig>),
|
|
1731
1741
|
};
|
|
1732
|
-
if (!["none", "tunnel-file"].includes(ingress.shape)) {
|
|
1742
|
+
if (!["none", "tunnel-file", "polling"].includes(ingress.shape)) {
|
|
1743
|
+
throw new Error(
|
|
1744
|
+
`config.toml [ingress]: shape must be "none", "tunnel-file" or "polling" (got ${JSON.stringify(ingress.shape)})`,
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
// The person's own credential has no webhook: GitHub delivers webhooks to
|
|
1748
|
+
// an App, and the person has none. So that mode is polling-only, and any
|
|
1749
|
+
// other shape is refused here rather than left to run half-wired.
|
|
1750
|
+
if (github.auth === "user" && ingress.shape !== "polling") {
|
|
1733
1751
|
throw new Error(
|
|
1734
|
-
`config.toml [
|
|
1752
|
+
`config.toml: [github] auth = "user" runs polling-only — set [ingress] shape = "polling" ` +
|
|
1753
|
+
`(got ${JSON.stringify(ingress.shape)}${raw.ingress === undefined ? ", the default for an absent [ingress]" : ""}). ` +
|
|
1754
|
+
`Webhooks and the webhook self-heal need a GitHub App: use auth = "app" for them.`,
|
|
1735
1755
|
);
|
|
1736
1756
|
}
|
|
1737
1757
|
const cfg: Config = {
|
package/src/daemon.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
DEFAULT_POSTING_RETRY_WINDOW_MIN,
|
|
19
19
|
loadConfig,
|
|
20
20
|
sandboxRoot,
|
|
21
|
+
webhooksEnabled,
|
|
21
22
|
type Config,
|
|
22
23
|
} from "./config.js";
|
|
23
24
|
import { dispatchOnce, type DispatchAdapter, type PreparedTree } from "./dispatch.js";
|
|
@@ -30,7 +31,13 @@ import {
|
|
|
30
31
|
} from "./eventloop.js";
|
|
31
32
|
import { MutationGate, type MutationIntent } from "@quickchatai/pi-quickreview/src/gate.js";
|
|
32
33
|
import { GitHubAdapter } from "./github.js";
|
|
33
|
-
import {
|
|
34
|
+
import { githubCredential } from "@quickchatai/pi-quickreview/src/githubAuth.js";
|
|
35
|
+
import {
|
|
36
|
+
botAccountLogin,
|
|
37
|
+
isPersonMode,
|
|
38
|
+
ownActorAlias,
|
|
39
|
+
ownLogins,
|
|
40
|
+
} from "@quickchatai/pi-quickreview/src/identity.js";
|
|
34
41
|
import { REVIEW_SET_ASIDE_STALE_WINDOW_MS, type FeedbackEvent } from "./feedback.js";
|
|
35
42
|
import { enforceLedgerFreshness } from "./ledgercheck.js";
|
|
36
43
|
import { readLedgerCarry } from "@quickchatai/pi-quickreview/src/ledgercarry.js";
|
|
@@ -57,7 +64,13 @@ import {
|
|
|
57
64
|
} from "./recheck.js";
|
|
58
65
|
import type { DispatchAnnouncer, VerdictAnnouncer } from "./slack/announce.js";
|
|
59
66
|
import { applyEvent } from "./statemachine.js";
|
|
60
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
buildPipelineStatus,
|
|
69
|
+
buildStatusSnapshot,
|
|
70
|
+
StateStore,
|
|
71
|
+
writeStatusJson,
|
|
72
|
+
type OpenPrsSnapshot,
|
|
73
|
+
} from "./state.js";
|
|
61
74
|
import { PiAuthWatch } from "@quickchatai/pi-quickreview/src/authwatch.js";
|
|
62
75
|
import { SessionCommandSupervisor } from "./session-command-supervisor.js";
|
|
63
76
|
import { readRoundProgress } from "./requiredpasses.js";
|
|
@@ -313,13 +326,18 @@ export interface Daemon {
|
|
|
313
326
|
firstHealthyReconcileAt(): Date | null;
|
|
314
327
|
}
|
|
315
328
|
|
|
329
|
+
/** The webhook HMAC secret — or "" under the polling shape, which takes no
|
|
330
|
+
* webhooks and needs no secret (the `/webhook` route then refuses every
|
|
331
|
+
* delivery, webhook.ts). `[github] auth = "user"` is always polling. */
|
|
316
332
|
function readWebhookSecret(cfg: Config): string {
|
|
333
|
+
if (!webhooksEnabled(cfg)) return "";
|
|
317
334
|
return readFileSync(join(cfg.github.secrets_dir, "webhook_secret"), "utf8").trim();
|
|
318
335
|
}
|
|
319
336
|
|
|
320
|
-
/** The real adapter:
|
|
337
|
+
/** The real adapter: credential (`[github] auth`) → mutation gate (dry-run
|
|
338
|
+
* aware) → adapter. */
|
|
321
339
|
function buildGitHubAdapter(cfg: Config, stateDir: string): GitHubAdapter {
|
|
322
|
-
const minter =
|
|
340
|
+
const minter = githubCredential(cfg.github);
|
|
323
341
|
const gate: MutationGate = new MutationGate(
|
|
324
342
|
cfg.dry_run,
|
|
325
343
|
join(stateDir, "dryrun"),
|
|
@@ -496,7 +514,8 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
|
|
|
496
514
|
dir: join(stateDir, "posting-receipts"),
|
|
497
515
|
repo: cfg.github.repo,
|
|
498
516
|
dryRun: cfg.dry_run,
|
|
499
|
-
botLogins:
|
|
517
|
+
botLogins: ownLogins(cfg.github),
|
|
518
|
+
personMode: isPersonMode(cfg.github),
|
|
500
519
|
adapter: deps.postingDelivery,
|
|
501
520
|
current: (job) => {
|
|
502
521
|
const pr = store.getPr(job.pr);
|
|
@@ -587,7 +606,7 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
|
|
|
587
606
|
log,
|
|
588
607
|
isDraining: () => store.drainState().draining,
|
|
589
608
|
botLogin: cfg.github.bot_login,
|
|
590
|
-
ownerActorAlias:
|
|
609
|
+
ownerActorAlias: ownActorAlias(cfg.github),
|
|
591
610
|
// The author asked for a person on a review thread.
|
|
592
611
|
//
|
|
593
612
|
// There is no park to put them in: the state event is a deliberate no-op
|
|
@@ -606,7 +625,7 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
|
|
|
606
625
|
{ type: "human_requested" },
|
|
607
626
|
{},
|
|
608
627
|
{
|
|
609
|
-
botLogin: cfg.github
|
|
628
|
+
botLogin: botAccountLogin(cfg.github),
|
|
610
629
|
reason: "pingpong:human-requested",
|
|
611
630
|
},
|
|
612
631
|
);
|
|
@@ -806,10 +825,13 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
|
|
|
806
825
|
const outcomes = new OutcomeCapture({
|
|
807
826
|
store,
|
|
808
827
|
botLogin: cfg.github.bot_login,
|
|
828
|
+
personMode: isPersonMode(cfg.github),
|
|
809
829
|
...(deps.outcomeGit ? { git: deps.outcomeGit } : {}),
|
|
810
830
|
...(deps.now ? { now: deps.now } : {}),
|
|
811
831
|
});
|
|
812
832
|
|
|
833
|
+
/** The open PRs the last listing reconcile pass saw (status.json `open_prs`). */
|
|
834
|
+
let openPrs: OpenPrsSnapshot | undefined;
|
|
813
835
|
let firstReconcileSeen = false;
|
|
814
836
|
let firstHealthyReconcileAt: Date | null = null;
|
|
815
837
|
// Deploy-window drain auto-lift: only the boot's first reconcile pass may
|
|
@@ -893,6 +915,8 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
|
|
|
893
915
|
// starts on the next pass to keep the boot pass short.
|
|
894
916
|
receiptSweep: !bootFirstPass,
|
|
895
917
|
});
|
|
918
|
+
// The board's "review a PR" list: kept from the last pass that listed.
|
|
919
|
+
if (report.open_list) openPrs = { listed_at: report.ran_at, prs: report.open_list };
|
|
896
920
|
log(
|
|
897
921
|
`reconcile: open=${report.open_prs} triggered=${report.triggered.length} ` +
|
|
898
922
|
`closed=${report.closed.length} captured=${report.captured_comments} ` +
|
|
@@ -1097,6 +1121,7 @@ export function createDaemon(deps: DaemonDeps = {}): Daemon {
|
|
|
1097
1121
|
...(job.deferred_at ? { deferred_since: job.deferred_at } : {}),
|
|
1098
1122
|
};
|
|
1099
1123
|
}
|
|
1124
|
+
if (openPrs) snapshot.open_prs = openPrs;
|
|
1100
1125
|
if (deps.runHost) snapshot.run_host = deps.runHost;
|
|
1101
1126
|
writeStatusJson(stateDir, snapshot);
|
|
1102
1127
|
} catch (err) {
|
package/src/debugroundpi.ts
CHANGED
|
@@ -53,7 +53,14 @@ import { CONTEXT_PACK_FILE } from "@quickchatai/pi-quickreview/src/contextpack.j
|
|
|
53
53
|
import { verifiedPayload } from "./session-command-payload.js";
|
|
54
54
|
import { DEBUG_START_MESSAGE } from "./engine/debugpi.js";
|
|
55
55
|
import { DEBUG_STATUS_NAME, debugSession, type DebugRoundSpec } from "./debuground.js";
|
|
56
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
onTerminal,
|
|
58
|
+
paneLaunch,
|
|
59
|
+
prepareBorrowedAgent,
|
|
60
|
+
readLogin,
|
|
61
|
+
takeoverEnv,
|
|
62
|
+
} from "./takeoverpi.js";
|
|
63
|
+
import { routeModelSpecForLogin } from "@quickchatai/pi-quickreview/src/modelroute.js";
|
|
57
64
|
|
|
58
65
|
export { DEBUG_START_MESSAGE };
|
|
59
66
|
|
|
@@ -225,6 +232,9 @@ export async function runDebugRoundPane(
|
|
|
225
232
|
return 3;
|
|
226
233
|
}
|
|
227
234
|
const env = debugRoundEnv(procEnv, spec);
|
|
235
|
+
// An OpenAI model runs on the OpenAI login the borrowed copy holds
|
|
236
|
+
// (packages/pi-plugin/src/modelroute.ts): the spec's own provider when it is there.
|
|
237
|
+
spec = { ...spec, model: routeModelSpecForLogin(spec.model, readLogin(spec.borrow_dir), env) };
|
|
228
238
|
const pi = paneLaunch(debugRoundArgv(spec), deps.piBin ?? spec.pi_bin);
|
|
229
239
|
const launch = toolIsolation(pi.bin, pi.argv, procEnv);
|
|
230
240
|
if ("refused" in launch) {
|
package/src/dispatch.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// plan and the order are built by the review planner (packages/pi-plugin/src/planner.ts), and the
|
|
16
16
|
// prompt renderer (packages/pi-plugin/src/templates.ts) is injected so this module has no content
|
|
17
17
|
// dependency.
|
|
18
|
+
import { botAccountLogin } from "@quickchatai/pi-quickreview/src/identity.js";
|
|
18
19
|
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
19
20
|
import { join, resolve } from "node:path";
|
|
20
21
|
import type { Config } from "./config.js";
|
|
@@ -1247,7 +1248,7 @@ async function dispatchUnlocked(deps: DispatchDeps): Promise<RoundOrder[]> {
|
|
|
1247
1248
|
);
|
|
1248
1249
|
}
|
|
1249
1250
|
const restored = restoreStandingLifecycle(store, row.number, standing, {
|
|
1250
|
-
botLogin: cfg.github
|
|
1251
|
+
botLogin: botAccountLogin(cfg.github),
|
|
1251
1252
|
now,
|
|
1252
1253
|
reason: standing ? "no-new-evidence" : "no-new-evidence:standing-unknown",
|
|
1253
1254
|
});
|
|
@@ -1329,7 +1330,7 @@ async function dispatchUnlocked(deps: DispatchDeps): Promise<RoundOrder[]> {
|
|
|
1329
1330
|
{ type: "verdict", verdict: standing },
|
|
1330
1331
|
{},
|
|
1331
1332
|
{
|
|
1332
|
-
botLogin: cfg.github
|
|
1333
|
+
botLogin: botAccountLogin(cfg.github),
|
|
1333
1334
|
now,
|
|
1334
1335
|
reason: `dispatch-short-circuit:${sc.reason}`,
|
|
1335
1336
|
},
|
|
@@ -2086,7 +2087,7 @@ async function dispatchUnlocked(deps: DispatchDeps): Promise<RoundOrder[]> {
|
|
|
2086
2087
|
row.number,
|
|
2087
2088
|
{ type: "dispatched" },
|
|
2088
2089
|
{},
|
|
2089
|
-
{ botLogin: cfg.github
|
|
2090
|
+
{ botLogin: botAccountLogin(cfg.github), now },
|
|
2090
2091
|
);
|
|
2091
2092
|
store.logEvent("round_dispatched", row.number, {
|
|
2092
2093
|
round_id: roundId,
|
|
@@ -2182,7 +2183,7 @@ async function dispatchUnlocked(deps: DispatchDeps): Promise<RoundOrder[]> {
|
|
|
2182
2183
|
row.number,
|
|
2183
2184
|
{ type: "dispatched" },
|
|
2184
2185
|
{},
|
|
2185
|
-
{ botLogin: cfg.github
|
|
2186
|
+
{ botLogin: botAccountLogin(cfg.github), now },
|
|
2186
2187
|
);
|
|
2187
2188
|
} catch (publishError) {
|
|
2188
2189
|
log(
|
package/src/engine/debugpi.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
|
25
25
|
import { join } from "node:path";
|
|
26
26
|
import { FINDING_CLUSTERS_ENTRY, extensionFile } from "../coreext.js";
|
|
27
27
|
import { GITHUB_TOKEN_ENVS } from "@quickchatai/pi-quickreview/src/envstrip.js";
|
|
28
|
+
import { routeModelSpecForLogin } from "@quickchatai/pi-quickreview/src/modelroute.js";
|
|
28
29
|
import { piCliPath } from "../picli.js";
|
|
29
30
|
import {
|
|
30
31
|
DEBUG_STATUS_FILE,
|
|
@@ -57,6 +58,15 @@ export const DEBUG_ROUND_EXTENSIONS = [
|
|
|
57
58
|
FINDING_CLUSTERS_ENTRY,
|
|
58
59
|
] as const;
|
|
59
60
|
|
|
61
|
+
/** The login file in an agent dir, as text; null when there is none. */
|
|
62
|
+
function loginIn(agentDir: string): string | null {
|
|
63
|
+
try {
|
|
64
|
+
return readFileSync(join(agentDir, "auth.json"), "utf8");
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
/** pi's first message: the extension command that starts the round. */
|
|
61
71
|
export const DEBUG_START_MESSAGE = "/qcr-round";
|
|
62
72
|
|
|
@@ -186,17 +196,19 @@ export async function runDebugRound(i: RoundSpec, deps: DebugRoundDeps = {}): Pr
|
|
|
186
196
|
// an earlier attempt's progress record is not this round's
|
|
187
197
|
rmSync(join(i.outDir, DEBUG_STATUS_FILE), { force: true });
|
|
188
198
|
log(`review: ${label} - debug round in pi, lenses ${prep.prompts.plannedLenses.join(" ")}`);
|
|
199
|
+
const env = debugEnv(deps.env ?? process.env, prep, {
|
|
200
|
+
order: join(i.outDir, "round-order.md"),
|
|
201
|
+
tasksDir: join(i.outDir, "tasks"),
|
|
202
|
+
});
|
|
189
203
|
const args = debugArgv({
|
|
190
204
|
root: i.root,
|
|
191
|
-
model
|
|
205
|
+
// An OpenAI model runs on the OpenAI login the run's agent dir holds
|
|
206
|
+
// (packages/pi-plugin/src/modelroute.ts).
|
|
207
|
+
model: routeModelSpecForLogin(i.cfg.models.conversation, loginIn(i.agentDir), env),
|
|
192
208
|
sysPath: prep.sysPath,
|
|
193
209
|
sessionDir: prep.sessionDir,
|
|
194
210
|
sessionName: prep.sessionName,
|
|
195
211
|
});
|
|
196
|
-
const env = debugEnv(deps.env ?? process.env, prep, {
|
|
197
|
-
order: join(i.outDir, "round-order.md"),
|
|
198
|
-
tasksDir: join(i.outDir, "tasks"),
|
|
199
|
-
});
|
|
200
212
|
// The whole pi runs as the tool uid, as the process route's did: its bash,
|
|
201
213
|
// and the lens sessions of its extension, get no capability and cannot read
|
|
202
214
|
// this process.
|
package/src/execboundary.ts
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
// · It detects; it never blocks. A detector that could wedge a review would
|
|
38
38
|
// be traded away the first time it misfired.
|
|
39
39
|
|
|
40
|
+
import { carriesOwnMarker } from "@quickchatai/pi-quickreview/src/identity.js";
|
|
40
41
|
import { createHash } from "node:crypto";
|
|
41
42
|
import { readFile, readdir } from "node:fs/promises";
|
|
42
43
|
import { join, resolve } from "node:path";
|
|
@@ -554,11 +555,22 @@ function hasIssuedMarker(body: string, commitId: string, issued: ReadonlySet<str
|
|
|
554
555
|
* every GitHub App posts as a `Bot`, and another installed review bot has no
|
|
555
556
|
* posting trace here by construction, so a type check would report each of
|
|
556
557
|
* its reviews (and raise the severe wake on its approvals) as ours. */
|
|
557
|
-
function isBotAuthored(
|
|
558
|
+
function isBotAuthored(
|
|
559
|
+
review: AuditedReview,
|
|
560
|
+
botLogin: string,
|
|
561
|
+
appSlug?: string,
|
|
562
|
+
personMode = false,
|
|
563
|
+
): boolean {
|
|
558
564
|
const login = review.user.toLowerCase();
|
|
565
|
+
// Person mode: the login is the person's, who reviews by hand too. Only a
|
|
566
|
+
// review carrying the reviewer's marker or footer is the reviewer's — so a
|
|
567
|
+
// write outside the gate that carries neither is invisible here (recorded
|
|
568
|
+
// in docs/exec-boundary.md), and the person's own approval never alarms.
|
|
569
|
+
if (personMode)
|
|
570
|
+
return login === botLogin.toLowerCase() && botLogin !== "" && carriesOwnMarker(review.body);
|
|
559
571
|
return (
|
|
560
572
|
login === botLogin.toLowerCase() ||
|
|
561
|
-
(appSlug !== undefined && login === `${appSlug.toLowerCase()}[bot]`)
|
|
573
|
+
(appSlug !== undefined && appSlug !== "" && login === `${appSlug.toLowerCase()}[bot]`)
|
|
562
574
|
);
|
|
563
575
|
}
|
|
564
576
|
|
|
@@ -573,7 +585,7 @@ export function auditUngatedReviews(
|
|
|
573
585
|
reviews: readonly AuditedReview[],
|
|
574
586
|
headSha: string,
|
|
575
587
|
traces: PostingTraces,
|
|
576
|
-
opts: { botLogin: string; appSlug?: string },
|
|
588
|
+
opts: { botLogin: string; appSlug?: string; personMode?: boolean },
|
|
577
589
|
): UngatedReview[] {
|
|
578
590
|
const head = headSha.toLowerCase();
|
|
579
591
|
const out: UngatedReview[] = [];
|
|
@@ -583,7 +595,7 @@ export function auditUngatedReviews(
|
|
|
583
595
|
// recorded in docs/exec-boundary.md rather than implied away.
|
|
584
596
|
let migrationExcusesLeft = traces.migrationPrecheck ? 1 : 0;
|
|
585
597
|
for (const r of reviews) {
|
|
586
|
-
if (!isBotAuthored(r, opts.botLogin, opts.appSlug)) continue;
|
|
598
|
+
if (!isBotAuthored(r, opts.botLogin, opts.appSlug, opts.personMode === true)) continue;
|
|
587
599
|
const state = (r.state ?? "").toUpperCase();
|
|
588
600
|
if (state === "PENDING" || state === "DISMISSED") continue;
|
|
589
601
|
if ((r.commit_id ?? "").toLowerCase() !== head) continue;
|