pi-pr-review 1.16.0 → 1.17.1
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/CHANGELOG.md +14 -0
- package/README.md +7 -5
- package/extensions/pr-review-subagent.ts +125 -15
- package/extensions/review-table.ts +21 -3
- package/lib/pr-review-artifacts.ts +15 -0
- package/lib/pr-review-deadlines.ts +33 -0
- package/lib/pr-review-publish.ts +51 -1
- package/package.json +1 -1
- package/prompts/pr-review.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.17.1](https://github.com/10ego/pi-pr-review/compare/v1.17.0...v1.17.1) (2026-08-31)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **review:** start batch deadline at reviewer dispatch ([#124](https://github.com/10ego/pi-pr-review/issues/124)) ([26a38af](https://github.com/10ego/pi-pr-review/commit/26a38af6559223c6403913de32026c7d2eee5d2b))
|
|
9
|
+
|
|
10
|
+
## [1.17.0](https://github.com/10ego/pi-pr-review/compare/v1.16.0...v1.17.0) (2026-08-29)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **review:** configure default review mode ([#122](https://github.com/10ego/pi-pr-review/issues/122)) ([52028be](https://github.com/10ego/pi-pr-review/commit/52028be9fcc91713e54378e3d50f31a3cd9cd642))
|
|
16
|
+
|
|
3
17
|
## [1.16.0](https://github.com/10ego/pi-pr-review/compare/v1.15.16...v1.16.0) (2026-08-28)
|
|
4
18
|
|
|
5
19
|
|
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ The semantic result is predictable human-readable Markdown in every mode. GitHub
|
|
|
59
59
|
| `/pr-review 123 --deep` | One integrated heavy reviewer for the whole PR. |
|
|
60
60
|
| `/pr-review 123 --include-closed` | Reviews a closed or merged PR without asking first. |
|
|
61
61
|
|
|
62
|
-
`--quick`, `--major-only`, `--balanced`, `--full`, and `--deep` are mutually exclusive.
|
|
62
|
+
`--quick`, `--major-only`, `--balanced`, `--full`, and `--deep` are mutually exclusive. When no mode flag is supplied, `/pr-review` uses the configured default mode; this is `balanced` until changed with `/pr-review-config`.
|
|
63
63
|
|
|
64
64
|
`--deep` trades parallel lens coverage for holistic judgment: a single heavy-tier reviewer receives the complete diff plus repository tools and reviews the change as one story—intent, approach, cross-file behavior, and test fit. It uses the same deadline, artifact, degradation, extraction, and publication machinery as every other mode. Without `--include-closed` or `--review-closed`, Pi asks before reviewing a non-open PR.
|
|
65
65
|
|
|
@@ -115,7 +115,7 @@ The viewer intentionally cannot send prompts, steering, or follow-ups to reviewe
|
|
|
115
115
|
|
|
116
116
|
## Configure models
|
|
117
117
|
|
|
118
|
-
`/pr-review-config` opens an interactive settings menu in the TUI. Use `/pr-review-config show` for a text summary or `key=value` arguments for direct changes.
|
|
118
|
+
`/pr-review-config` opens an interactive settings menu in the TUI. Its **default review mode** selector replaces the former parallelism control: reviewer count and concurrency are derived from the selected mode. Use `/pr-review-config show` for a text summary or `key=value` arguments for direct changes.
|
|
119
119
|
|
|
120
120
|
| Tier | Purpose |
|
|
121
121
|
|---|---|
|
|
@@ -126,6 +126,7 @@ The viewer intentionally cannot send prompts, steering, or follow-ups to reviewe
|
|
|
126
126
|
Common settings:
|
|
127
127
|
|
|
128
128
|
```text
|
|
129
|
+
/pr-review-config default_review_mode=balanced
|
|
129
130
|
/pr-review-config light=provider/model heavy=provider/model:high
|
|
130
131
|
/pr-review-config heavy_fallbacks=provider/backup:high,provider/backup2
|
|
131
132
|
/pr-review-config light_thinking=low medium_thinking=medium heavy_thinking=high
|
|
@@ -147,12 +148,13 @@ Configuration is stored in:
|
|
|
147
148
|
- user scope: `~/.pi/agent/pr-review.json`;
|
|
148
149
|
- project scope: `<repo>/.pi/pr-review.json`, applied only when the project is trusted.
|
|
149
150
|
|
|
150
|
-
A trusted project can override model, tool, and publication settings. Verification profiles are always user-only.
|
|
151
|
+
A trusted project can override the default review mode, model, tool, and publication settings. Explicit `/pr-review --quick|--balanced|--full|--deep` flags override the configured default. Verification profiles are always user-only.
|
|
151
152
|
|
|
152
153
|
Example:
|
|
153
154
|
|
|
154
155
|
```json
|
|
155
156
|
{
|
|
157
|
+
"defaultReviewMode": "balanced",
|
|
156
158
|
"tiers": {
|
|
157
159
|
"light": "provider/fast-model",
|
|
158
160
|
"medium": "provider/balanced-model",
|
|
@@ -189,13 +191,13 @@ Example:
|
|
|
189
191
|
}
|
|
190
192
|
```
|
|
191
193
|
|
|
192
|
-
Every invocation has a host-owned monotonic 15-minute hard cap, including the two GitHub identity/lifecycle preflights, synthesis, termination grace, and reserved cleanup. The dependent preflight calls share the one invocation budget, so they cannot each add an independent command timeout before review timing begins. Defaults bound light/medium/heavy attempts to 3/6/8 minutes, a fallback attempt to 3 minutes, and the concurrent batch to 12 minutes. The complete `deadlines` object may be replaced at user scope or by a trusted project; partial, malformed, non-integer, out-of-range, or internally inconsistent objects are rejected as a unit and the last valid/default finite budget remains active. Supported inclusive ranges are: attempts 30–720 seconds, fallback 30–360 seconds, batch 60–840 seconds, synthesis 10–120 seconds, total 120–1200 seconds, TERM grace 0.1–15 seconds, cleanup reserve 1–30 seconds, and minimum useful fallback 10–120 seconds. Minimum fallback must not exceed its attempt cap, and batch + synthesis + termination grace + cleanup must fit inside total.
|
|
194
|
+
Every invocation has a host-owned monotonic 15-minute hard cap, including the two GitHub identity/lifecycle preflights, parent orchestration, synthesis, termination grace, and reserved cleanup. The dependent preflight calls share the one invocation budget, so they cannot each add an independent command timeout before review timing begins. The reviewer batch window activates once, at the first reviewer dispatch: preflight and diff capture remain charged to the total cap but cannot exhaust `batchMs` before any reviewer starts. The activated batch is still truncated by the original total deadline and its synthesis/termination/cleanup reserves. Defaults bound light/medium/heavy attempts to 3/6/8 minutes, a fallback attempt to 3 minutes, and the concurrent batch to 12 minutes. The complete `deadlines` object may be replaced at user scope or by a trusted project; partial, malformed, non-integer, out-of-range, or internally inconsistent objects are rejected as a unit and the last valid/default finite budget remains active. Supported inclusive ranges are: attempts 30–720 seconds, fallback 30–360 seconds, batch 60–840 seconds, synthesis 10–120 seconds, total 120–1200 seconds, TERM grace 0.1–15 seconds, cleanup reserve 1–30 seconds, and minimum useful fallback 10–120 seconds. Minimum fallback must not exceed its attempt cap, and batch + synthesis + termination grace + cleanup must fit inside total.
|
|
193
195
|
|
|
194
196
|
A timed-out or retryable quota/rate-limit/capacity lane may start at most one configured fallback attempt. It starts only when at least `minimumFallbackMs` plus cleanup reserve remains; the host never changes the configured model, thinking level, or tool policy to save time. If a tier is unset, its existing nearest-configured-tier/Pi-default behavior is unchanged.
|
|
195
197
|
|
|
196
198
|
On an attempt deadline the host records timeout separately from user cancellation, sends TERM to the original child, waits only `terminationGraceMs`, then sends KILL if no exit was observed and stops draining after the cleanup reserve. Partial assistant text and telemetry survive this lifecycle. The synthesis cap arms only once review work goes quiet: any turn that starts or any review tool that runs again while the cap is armed defers it, and it re-arms from the next turn end, so early review-tool turns (for example verification discovery) cannot starve later heavy lanes. Batch/total expiry stops queued work and waiting lanes; completed and partial artifacts proceed to Markdown synthesis or deterministic lane assembly, identify every incomplete reviewer, and remain eligible for the safe body-only `COMMENT` publication path. A timed-out lane is never reported as `NO FINDINGS` or full coverage, and a lane ended by the host total or synthesis deadline is disclosed with that kind (`deadline_expired`) instead of being mistaken for its own attempt deadline expiring. Host lane artifacts are authoritative for completeness in both directions: a false assistant completion claim cannot upgrade incomplete lanes, and a paraphrased or omitted `Lane completeness` line cannot downgrade a host-complete batch away from the concise renderer.
|
|
197
199
|
|
|
198
|
-
Initial operating targets are ordinary-review p50 ≤ 6 minutes and p95 ≤ 12 minutes, and large-review p50 ≤ 10 minutes and p95 ≤ 14 minutes, with the 15-minute hard cap authoritative. Invocation telemetry starts before GitHub preflight and records configured deadline source/caps, termination grace, cleanup reserve, and active wall time; batch details record first event/output timing, lifecycle counts, configured and effective batch-truncated attempt deadlines, fallback starts/budget rejections, external total/synthesis deadline expiries per lane, and termination grace/escalation data. These are initial production targets, not a promise that every provider completes before its host deadline.
|
|
200
|
+
Initial operating targets are ordinary-review p50 ≤ 6 minutes and p95 ≤ 12 minutes, and large-review p50 ≤ 10 minutes and p95 ≤ 14 minutes, with the 15-minute hard cap authoritative. Invocation telemetry starts before GitHub preflight and records configured deadline source/caps, termination grace, cleanup reserve, and active wall time; batch details record invocation time consumed before each attempt, batch/total time remaining at dispatch, first event/output timing, lifecycle counts, configured and effective batch-truncated attempt deadlines, fallback starts/budget rejections, external total/synthesis deadline expiries per lane, and termination grace/escalation data. These are initial production targets, not a promise that every provider completes before its host deadline.
|
|
199
201
|
|
|
200
202
|
Ordinary reviewer output is reconstructed from every text part of the authoritative final assistant message in order. A zero process exit is not enough to mark a lane complete: completion also requires a terminal `stop` and the expected lane sections (or an explicitly emitted `NO FINDINGS.` where that response is allowed). Empty success is never synthesized into `NO FINDINGS.`; length-limited, malformed, timed-out, and failed attempts retain their raw text with an explicit lifecycle. Batch details include raw text, attempt artifacts, lifecycle counts and elapsed totals, while the displayed batch remains in deterministic input order.
|
|
201
203
|
|
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
type ReviewLaneLifecycle,
|
|
57
57
|
} from "../lib/pr-review-artifacts.ts";
|
|
58
58
|
import { runWithConcurrency } from "../lib/pr-review-concurrency.ts";
|
|
59
|
-
import { attemptDeadline, fallbackBudget, type ReviewBudget } from "../lib/pr-review-deadlines.ts";
|
|
59
|
+
import { activateReviewBatch, attemptDeadline, fallbackBudget, type ReviewBudget } from "../lib/pr-review-deadlines.ts";
|
|
60
60
|
import { buildExtractionSystemPrompt, buildExtractionTask, MAX_EXTRACTION_OUTPUT_BYTES } from "../lib/pr-review-extract.ts";
|
|
61
61
|
import { loadReviewContext } from "../lib/pr-review-context.ts";
|
|
62
62
|
import {
|
|
@@ -91,6 +91,8 @@ import {
|
|
|
91
91
|
resolveAllowStalePublishSetting,
|
|
92
92
|
resolveAutoPostSetting,
|
|
93
93
|
resolveApproveMaxPriorityLevelSetting,
|
|
94
|
+
resolveDefaultReviewModeSetting,
|
|
95
|
+
REVIEW_MODES,
|
|
94
96
|
type ApproveMaxPriorityLevel,
|
|
95
97
|
type ReviewMode,
|
|
96
98
|
} from "../lib/pr-review-publish.ts";
|
|
@@ -175,6 +177,8 @@ const TIER_PURPOSE: Record<Tier, string> = {
|
|
|
175
177
|
};
|
|
176
178
|
|
|
177
179
|
interface PrReviewConfig {
|
|
180
|
+
/** Review topology used when /pr-review has no explicit mode flag. */
|
|
181
|
+
defaultReviewMode: ReviewMode;
|
|
178
182
|
/** Tier label -> model spec (e.g. "anthropic/model", "openai/model:high"). */
|
|
179
183
|
tiers: Partial<Record<Tier, string>>;
|
|
180
184
|
/** Tier label -> ordered fallback model specs used for quota/capacity failures. */
|
|
@@ -258,6 +262,17 @@ function normalizeToolPolicies(
|
|
|
258
262
|
return out;
|
|
259
263
|
}
|
|
260
264
|
|
|
265
|
+
function resolveDefaultReviewModeForContext(ctx: Pick<ExtensionContext, "cwd" | "isProjectTrusted">) {
|
|
266
|
+
const user = readConfigFile(userConfigPath());
|
|
267
|
+
let project: Partial<PrReviewConfig> | undefined;
|
|
268
|
+
try {
|
|
269
|
+
if (ctx.isProjectTrusted()) project = readConfigFile(projectConfigPath(ctx.cwd));
|
|
270
|
+
} catch {
|
|
271
|
+
/* user config only */
|
|
272
|
+
}
|
|
273
|
+
return resolveDefaultReviewModeSetting(user, project);
|
|
274
|
+
}
|
|
275
|
+
|
|
261
276
|
function resolveAutoPostForContext(ctx: Pick<ExtensionContext, "cwd" | "isProjectTrusted">) {
|
|
262
277
|
const user = readConfigFile(userConfigPath());
|
|
263
278
|
let project: Partial<PrReviewConfig> | undefined;
|
|
@@ -315,7 +330,9 @@ function loadConfig(ctx: Pick<ExtensionContext, "cwd" | "isProjectTrusted">): Pr
|
|
|
315
330
|
const allowStale = resolveAllowStalePublishSetting(user, project);
|
|
316
331
|
const allowStaleApprovals = resolveAllowStaleApprovalsSetting(user, project);
|
|
317
332
|
const approveMaxPriority = resolveApproveMaxPriorityLevelSetting(user, project);
|
|
333
|
+
const defaultReviewMode = resolveDefaultReviewModeSetting(user, project);
|
|
318
334
|
return {
|
|
335
|
+
defaultReviewMode: defaultReviewMode.valid ? defaultReviewMode.value : "balanced",
|
|
319
336
|
tiers: { ...(user.tiers ?? {}), ...(project.tiers ?? {}) },
|
|
320
337
|
fallbacks: { ...normalizeFallbacks(user.fallbacks, true), ...normalizeFallbacks(project.fallbacks, true) },
|
|
321
338
|
thinkingLevels: {
|
|
@@ -345,7 +362,9 @@ function readUserConfig(): PrReviewConfig {
|
|
|
345
362
|
const allowStale = resolveAllowStalePublishSetting(raw);
|
|
346
363
|
const allowStaleApprovals = resolveAllowStaleApprovalsSetting(raw);
|
|
347
364
|
const approveMaxPriority = resolveApproveMaxPriorityLevelSetting(raw);
|
|
365
|
+
const defaultReviewMode = resolveDefaultReviewModeSetting(raw);
|
|
348
366
|
return {
|
|
367
|
+
defaultReviewMode: defaultReviewMode.valid ? defaultReviewMode.value : "balanced",
|
|
349
368
|
tiers: { ...(raw.tiers ?? {}) },
|
|
350
369
|
fallbacks: normalizeFallbacks(raw.fallbacks),
|
|
351
370
|
thinkingLevels: normalizeThinkingLevels(raw.thinkingLevels, "User pr-review config"),
|
|
@@ -891,6 +910,12 @@ interface ModelAttemptReport {
|
|
|
891
910
|
forcedTermination?: boolean;
|
|
892
911
|
deadlineMs?: number;
|
|
893
912
|
configuredDeadlineMs?: number;
|
|
913
|
+
/** Invocation time already consumed before this model attempt was scheduled. */
|
|
914
|
+
budgetElapsedBeforeAttemptMs?: number;
|
|
915
|
+
/** Remaining absolute reviewer-batch window at scheduling time; may be negative. */
|
|
916
|
+
batchRemainingBeforeAttemptMs?: number;
|
|
917
|
+
/** Remaining total invocation window at scheduling time. */
|
|
918
|
+
totalRemainingBeforeAttemptMs?: number;
|
|
894
919
|
}
|
|
895
920
|
|
|
896
921
|
interface SubagentPassResult {
|
|
@@ -967,16 +992,32 @@ async function runSubagentAttempt(
|
|
|
967
992
|
onText?: (text: string) => void,
|
|
968
993
|
beforeSpawn?: () => boolean,
|
|
969
994
|
budget?: ReviewBudget,
|
|
970
|
-
): Promise<{
|
|
995
|
+
): Promise<{
|
|
996
|
+
result: RunResult;
|
|
997
|
+
notice: string;
|
|
998
|
+
elapsedMs: number;
|
|
999
|
+
deadlineMs?: number;
|
|
1000
|
+
budgetElapsedBeforeAttemptMs?: number;
|
|
1001
|
+
batchRemainingBeforeAttemptMs?: number;
|
|
1002
|
+
totalRemainingBeforeAttemptMs?: number;
|
|
1003
|
+
}> {
|
|
971
1004
|
let tmp: { dir: string; filePath: string } | undefined;
|
|
972
1005
|
const startedAt = monotonicNow();
|
|
1006
|
+
const budgetElapsedBeforeAttemptMs = budget ? startedAt - budget.startedAtMs : undefined;
|
|
1007
|
+
const batchRemainingBeforeAttemptMs = budget ? budget.batchDeadlineMs - startedAt : undefined;
|
|
1008
|
+
const totalRemainingBeforeAttemptMs = budget ? budget.totalDeadlineMs - startedAt : undefined;
|
|
973
1009
|
const deadlineAtMs = budget
|
|
974
1010
|
? attemptDeadline(budget, pass.tier, attempt.kind === "fallback", () => startedAt)
|
|
975
1011
|
: undefined;
|
|
976
1012
|
const deadlineMs = deadlineAtMs === undefined ? undefined : Math.max(0, deadlineAtMs - startedAt);
|
|
977
1013
|
try {
|
|
978
1014
|
if (deadlineAtMs !== undefined && deadlineAtMs <= startedAt) {
|
|
979
|
-
throw new Error(
|
|
1015
|
+
throw new Error(
|
|
1016
|
+
`Review attempt timed out before it could start within the batch deadline ` +
|
|
1017
|
+
`(invocation elapsed ${Math.round(budgetElapsedBeforeAttemptMs!)}ms; ` +
|
|
1018
|
+
`batch remaining ${Math.round(batchRemainingBeforeAttemptMs!)}ms; ` +
|
|
1019
|
+
`total remaining ${Math.round(totalRemainingBeforeAttemptMs!)}ms).`,
|
|
1020
|
+
);
|
|
980
1021
|
}
|
|
981
1022
|
const args = buildReviewBaseArgs();
|
|
982
1023
|
if (attempt.spec) args.push("--model", attempt.spec);
|
|
@@ -1045,13 +1086,24 @@ async function runSubagentAttempt(
|
|
|
1045
1086
|
fileBackedAccessObserved = !!pass.fileBackedRequiredReads?.length &&
|
|
1046
1087
|
pass.fileBackedRequiredReads.every((range) => completedFileBackedReads.has(`${range.offset}:${range.limit}`));
|
|
1047
1088
|
result.fileBackedAccessObserved = fileBackedAccessObserved;
|
|
1048
|
-
return {
|
|
1089
|
+
return {
|
|
1090
|
+
result,
|
|
1091
|
+
notice: noticeForAttempt(pass.tier, attempt),
|
|
1092
|
+
elapsedMs: monotonicNow() - startedAt,
|
|
1093
|
+
deadlineMs,
|
|
1094
|
+
budgetElapsedBeforeAttemptMs,
|
|
1095
|
+
batchRemainingBeforeAttemptMs,
|
|
1096
|
+
totalRemainingBeforeAttemptMs,
|
|
1097
|
+
};
|
|
1049
1098
|
} catch (e) {
|
|
1050
1099
|
return {
|
|
1051
1100
|
result: { text: "", exitCode: 1, stderr: "", errorMessage: errMessage(e), toolElapsedMs: 0, toolCallCount: 0 },
|
|
1052
1101
|
notice: noticeForAttempt(pass.tier, attempt),
|
|
1053
1102
|
elapsedMs: monotonicNow() - startedAt,
|
|
1054
1103
|
deadlineMs,
|
|
1104
|
+
budgetElapsedBeforeAttemptMs,
|
|
1105
|
+
batchRemainingBeforeAttemptMs,
|
|
1106
|
+
totalRemainingBeforeAttemptMs,
|
|
1055
1107
|
};
|
|
1056
1108
|
} finally {
|
|
1057
1109
|
if (tmp) {
|
|
@@ -1200,6 +1252,9 @@ function retainPassArtifact(pass: SubagentPassRequest, result: SubagentPassResul
|
|
|
1200
1252
|
forcedTermination: attempt.forcedTermination,
|
|
1201
1253
|
deadlineMs: attempt.deadlineMs,
|
|
1202
1254
|
configuredDeadlineMs: attempt.configuredDeadlineMs,
|
|
1255
|
+
budgetElapsedBeforeAttemptMs: attempt.budgetElapsedBeforeAttemptMs,
|
|
1256
|
+
batchRemainingBeforeAttemptMs: attempt.batchRemainingBeforeAttemptMs,
|
|
1257
|
+
totalRemainingBeforeAttemptMs: attempt.totalRemainingBeforeAttemptMs,
|
|
1203
1258
|
})),
|
|
1204
1259
|
fallbackUsed: result.fallbackUsed,
|
|
1205
1260
|
elapsedMs: result.elapsedMs,
|
|
@@ -1244,7 +1299,15 @@ async function runSubagentPass(
|
|
|
1244
1299
|
fallbackBudgetRejected = true;
|
|
1245
1300
|
break;
|
|
1246
1301
|
}
|
|
1247
|
-
const {
|
|
1302
|
+
const {
|
|
1303
|
+
result,
|
|
1304
|
+
notice,
|
|
1305
|
+
elapsedMs,
|
|
1306
|
+
deadlineMs,
|
|
1307
|
+
budgetElapsedBeforeAttemptMs,
|
|
1308
|
+
batchRemainingBeforeAttemptMs,
|
|
1309
|
+
totalRemainingBeforeAttemptMs,
|
|
1310
|
+
} = await runSubagentAttempt(
|
|
1248
1311
|
config,
|
|
1249
1312
|
ctx,
|
|
1250
1313
|
pass,
|
|
@@ -1301,6 +1364,9 @@ async function runSubagentPass(
|
|
|
1301
1364
|
configuredDeadlineMs: budget
|
|
1302
1365
|
? (attempt.kind === "fallback" ? budget.config.fallbackAttemptMs : budget.config.attemptMs[tier])
|
|
1303
1366
|
: undefined,
|
|
1367
|
+
budgetElapsedBeforeAttemptMs,
|
|
1368
|
+
batchRemainingBeforeAttemptMs,
|
|
1369
|
+
totalRemainingBeforeAttemptMs,
|
|
1304
1370
|
});
|
|
1305
1371
|
|
|
1306
1372
|
if (lifecycle === "complete") {
|
|
@@ -1748,6 +1814,7 @@ export default function registerPrReviewSubagents(
|
|
|
1748
1814
|
});
|
|
1749
1815
|
const artifactPublisher = loopCoordinator.createArtifactPublisher(lease, ctx);
|
|
1750
1816
|
const config = loadConfig(ctx);
|
|
1817
|
+
const reviewBudget = lease.budget ? activateReviewBatch(lease.budget) : undefined;
|
|
1751
1818
|
const result = await runSubagentPass(
|
|
1752
1819
|
config,
|
|
1753
1820
|
ctx,
|
|
@@ -1766,7 +1833,7 @@ export default function registerPrReviewSubagents(
|
|
|
1766
1833
|
executionSignal,
|
|
1767
1834
|
(text) => onUpdate?.({ content: [{ type: "text", text }] }),
|
|
1768
1835
|
() => loopCoordinator.isLeaseActive(lease, ctx),
|
|
1769
|
-
|
|
1836
|
+
reviewBudget,
|
|
1770
1837
|
);
|
|
1771
1838
|
|
|
1772
1839
|
const warnings = [...thinkingWarnings(config, [tier]), ...(lease.budget?.warnings ?? [])];
|
|
@@ -1959,6 +2026,7 @@ export default function registerPrReviewSubagents(
|
|
|
1959
2026
|
// reviewer may run concurrently, and no caller/model tuning knob exists.
|
|
1960
2027
|
const reviewerConcurrency = passes.length;
|
|
1961
2028
|
const config = loadConfig(ctx);
|
|
2029
|
+
const reviewBudget = lease.budget ? activateReviewBatch(lease.budget) : undefined;
|
|
1962
2030
|
const batchStartedAt = monotonicNow();
|
|
1963
2031
|
|
|
1964
2032
|
const tierPriority: Record<Tier, number> = { heavy: 0, medium: 1, light: 2 };
|
|
@@ -1974,7 +2042,7 @@ export default function registerPrReviewSubagents(
|
|
|
1974
2042
|
executionSignal,
|
|
1975
2043
|
undefined,
|
|
1976
2044
|
() => loopCoordinator.isLeaseActive(lease, ctx),
|
|
1977
|
-
|
|
2045
|
+
reviewBudget,
|
|
1978
2046
|
);
|
|
1979
2047
|
const endOffsetMs = monotonicNow() - batchStartedAt;
|
|
1980
2048
|
result.startOffsetMs = startOffsetMs;
|
|
@@ -2072,7 +2140,7 @@ export default function registerPrReviewSubagents(
|
|
|
2072
2140
|
});
|
|
2073
2141
|
|
|
2074
2142
|
pi.registerCommand("pr-review-config", {
|
|
2075
|
-
description: "Open review
|
|
2143
|
+
description: "Open review settings, or set the default mode, models, thinking, and publication policy",
|
|
2076
2144
|
handler: async (args, ctx) => {
|
|
2077
2145
|
// Extension commands execute before input events, so revoke explicitly.
|
|
2078
2146
|
loopCoordinator.clear();
|
|
@@ -2085,7 +2153,7 @@ export default function registerPrReviewSubagents(
|
|
|
2085
2153
|
}
|
|
2086
2154
|
|
|
2087
2155
|
try {
|
|
2088
|
-
// Direct set: `/pr-review-config light=... heavy
|
|
2156
|
+
// Direct set: `/pr-review-config default_review_mode=full light=... heavy=...`
|
|
2089
2157
|
if (parsed.hasChanges) {
|
|
2090
2158
|
const next = applyConfigPatch(readUserConfig(), parsed.patch);
|
|
2091
2159
|
writeUserConfig(next);
|
|
@@ -2145,6 +2213,7 @@ function shouldOpenConfigMenu(args: string, ctx: ExtensionContext): boolean {
|
|
|
2145
2213
|
}
|
|
2146
2214
|
|
|
2147
2215
|
const CONFIG_COMPLETIONS: Array<{ value: string; label: string }> = [
|
|
2216
|
+
{ value: "default_review_mode=", label: "default_review_mode=<quick|balanced|full|deep> — topology used when no mode flag is supplied" },
|
|
2148
2217
|
...TIERS.map((t) => ({ value: `${t}=`, label: `${t}=<model> — ${TIER_PURPOSE[t]}` })),
|
|
2149
2218
|
...TIERS.map((t) => ({
|
|
2150
2219
|
value: `${t}_fallbacks=`,
|
|
@@ -2233,6 +2302,7 @@ function tierFromCompoundKey(key: string): Tier {
|
|
|
2233
2302
|
}
|
|
2234
2303
|
|
|
2235
2304
|
interface ConfigPatch {
|
|
2305
|
+
defaultReviewMode?: ReviewMode;
|
|
2236
2306
|
tiers: Partial<Record<Tier, string | null>>;
|
|
2237
2307
|
fallbacks: Partial<Record<Tier, string[] | null>>;
|
|
2238
2308
|
thinkingLevels: Partial<Record<Tier, ThinkingLevel | null>>;
|
|
@@ -2256,7 +2326,11 @@ function parseConfigArgs(args: string): { patch: ConfigPatch; hasChanges: boolea
|
|
|
2256
2326
|
}
|
|
2257
2327
|
const key = token.slice(0, eq).replace(/^--?/, "").toLowerCase();
|
|
2258
2328
|
const value = token.slice(eq + 1);
|
|
2259
|
-
if (
|
|
2329
|
+
if (key === "default_review_mode" || key === "defaultreviewmode" || key === "default-review-mode") {
|
|
2330
|
+
const normalized = value.toLowerCase();
|
|
2331
|
+
if (REVIEW_MODES.includes(normalized as ReviewMode)) patch.defaultReviewMode = normalized as ReviewMode;
|
|
2332
|
+
else errors.push(`invalid ${key} "${value}" (expected ${REVIEW_MODES.join("|")})`);
|
|
2333
|
+
} else if ((TIERS as string[]).includes(key)) {
|
|
2260
2334
|
patch.tiers[key as Tier] = value === "" || value === "unset" ? null : value;
|
|
2261
2335
|
} else if (isFallbackKey(key)) {
|
|
2262
2336
|
const tier = tierFromCompoundKey(key);
|
|
@@ -2308,11 +2382,12 @@ function parseConfigArgs(args: string): { patch: ConfigPatch; hasChanges: boolea
|
|
|
2308
2382
|
patch.tools = splitCommaList(value);
|
|
2309
2383
|
} else {
|
|
2310
2384
|
errors.push(
|
|
2311
|
-
`unknown key "${key}" (expected light|medium|heavy|<tier>_fallbacks|<tier>_thinking|<tier>_tool_policy|auto_post_reviews|allow_stale_publish|allow_stale_approvals|approve_max_priority_level|tools)`,
|
|
2385
|
+
`unknown key "${key}" (expected default_review_mode|light|medium|heavy|<tier>_fallbacks|<tier>_thinking|<tier>_tool_policy|auto_post_reviews|allow_stale_publish|allow_stale_approvals|approve_max_priority_level|tools)`,
|
|
2312
2386
|
);
|
|
2313
2387
|
}
|
|
2314
2388
|
}
|
|
2315
2389
|
const hasChanges =
|
|
2390
|
+
patch.defaultReviewMode !== undefined ||
|
|
2316
2391
|
Object.keys(patch.tiers).length > 0 ||
|
|
2317
2392
|
Object.keys(patch.fallbacks).length > 0 ||
|
|
2318
2393
|
Object.keys(patch.thinkingLevels).length > 0 ||
|
|
@@ -2327,6 +2402,7 @@ function parseConfigArgs(args: string): { patch: ConfigPatch; hasChanges: boolea
|
|
|
2327
2402
|
|
|
2328
2403
|
function applyConfigPatch(base: PrReviewConfig, patch: ConfigPatch): PrReviewConfig {
|
|
2329
2404
|
const next: PrReviewConfig = {
|
|
2405
|
+
defaultReviewMode: base.defaultReviewMode,
|
|
2330
2406
|
tiers: { ...base.tiers },
|
|
2331
2407
|
fallbacks: normalizeFallbacks(base.fallbacks),
|
|
2332
2408
|
thinkingLevels: normalizeThinkingLevels(base.thinkingLevels, "PR review config"),
|
|
@@ -2339,6 +2415,7 @@ function applyConfigPatch(base: PrReviewConfig, patch: ConfigPatch): PrReviewCon
|
|
|
2339
2415
|
tools: [...base.tools],
|
|
2340
2416
|
deadlines: base.deadlines,
|
|
2341
2417
|
};
|
|
2418
|
+
if (patch.defaultReviewMode !== undefined) next.defaultReviewMode = patch.defaultReviewMode;
|
|
2342
2419
|
for (const tier of TIERS) {
|
|
2343
2420
|
if (tier in patch.tiers) {
|
|
2344
2421
|
const value = patch.tiers[tier];
|
|
@@ -2388,6 +2465,7 @@ function summarizeConfig(
|
|
|
2388
2465
|
} catch {
|
|
2389
2466
|
/* ignore */
|
|
2390
2467
|
}
|
|
2468
|
+
const defaultReviewMode = resolveDefaultReviewModeForContext(ctx);
|
|
2391
2469
|
const autoPost = resolveAutoPostForContext(ctx);
|
|
2392
2470
|
const allowStale = resolveAllowStaleForContext(ctx);
|
|
2393
2471
|
const allowStaleApprovals = resolveAllowStaleApprovalsForContext(ctx);
|
|
@@ -2401,6 +2479,7 @@ function summarizeConfig(
|
|
|
2401
2479
|
(t) =>
|
|
2402
2480
|
`| \`${t}\` | ${user.tiers[t] ? `\`${user.tiers[t]}\`` : "_unset_"} | ${effective.tiers[t] ? `\`${effective.tiers[t]}\`` : "_pi default_"} | ${TIER_PURPOSE[t]} |`,
|
|
2403
2481
|
),
|
|
2482
|
+
`| \`defaultReviewMode\` | \`${user.defaultReviewMode}\` | \`${effective.defaultReviewMode}\` (${defaultReviewMode.source}) | topology used when no explicit mode flag is supplied; default \`balanced\` |`,
|
|
2404
2483
|
`| \`autoPostReviews\` | \`${user.autoPostReviews}\` | \`${effective.autoPostReviews}\` (${autoPost.source}) | automatically post one GitHub review; default \`false\` |`,
|
|
2405
2484
|
`| \`allowStalePublish\` | \`${user.allowStalePublish}\` | \`${effective.allowStalePublish}\` (${allowStale.source}) | permit body-only stale publication with reviewed/current SHAs; default \`true\` |`,
|
|
2406
2485
|
`| \`allowStaleApprovals\` | \`${user.allowStaleApprovals}\` | \`${effective.allowStaleApprovals}\` (${allowStaleApprovals.source}) | permit qualified stale fully parsed reviews to record APPROVE; default \`false\` |`,
|
|
@@ -2420,6 +2499,10 @@ function summarizeConfig(
|
|
|
2420
2499
|
if (projectPath) lines.push(`Project overlay (trusted): \`${projectPath}\``);
|
|
2421
2500
|
const warnings = thinkingWarnings(effective);
|
|
2422
2501
|
if (warnings.length) lines.push("", ...warnings);
|
|
2502
|
+
if (!defaultReviewMode.valid) lines.push(`Default review mode config error: ${defaultReviewMode.error}`);
|
|
2503
|
+
else if (defaultReviewMode.source === "project") {
|
|
2504
|
+
lines.push("Default review mode is controlled by the trusted project overlay; this command edits user config only.");
|
|
2505
|
+
}
|
|
2423
2506
|
if (!autoPost.valid) lines.push(`Automatic posting config error: ${autoPost.error}`);
|
|
2424
2507
|
else if (autoPost.source === "project") {
|
|
2425
2508
|
lines.push("Automatic posting is controlled by the trusted project overlay; this command edits user config only.");
|
|
@@ -2441,7 +2524,8 @@ function summarizeConfig(
|
|
|
2441
2524
|
"## Usage",
|
|
2442
2525
|
"- Open the settings menu: `/pr-review-config`",
|
|
2443
2526
|
"- Print this summary: `/pr-review-config show`",
|
|
2444
|
-
"- Set
|
|
2527
|
+
"- Set the no-flag review topology: `/pr-review-config default_review_mode=balanced`",
|
|
2528
|
+
"- Set models directly: `/pr-review-config light=provider/model heavy=provider/model:high`",
|
|
2445
2529
|
"- Set fallback chain: `/pr-review-config heavy_fallbacks=provider/backup:high,provider/backup2`",
|
|
2446
2530
|
"- Set tier thinking: `/pr-review-config light_thinking=low medium_thinking=medium heavy_thinking=high`",
|
|
2447
2531
|
"- Enable automatic GitHub review posting: `/pr-review-config auto_post_reviews=true`",
|
|
@@ -2570,6 +2654,13 @@ function configMenuItems(cfg: PrReviewConfig, available: string[]): SettingItem[
|
|
|
2570
2654
|
const current = cfg.tools.join(",");
|
|
2571
2655
|
const toolValues = [current, ...TOOLS_PRESETS.filter((p) => p !== current)];
|
|
2572
2656
|
return [
|
|
2657
|
+
{
|
|
2658
|
+
id: "default_review_mode",
|
|
2659
|
+
label: "default review mode",
|
|
2660
|
+
description: "Topology used when /pr-review has no explicit mode flag. Enter/Space cycles values.",
|
|
2661
|
+
currentValue: cfg.defaultReviewMode,
|
|
2662
|
+
values: [...REVIEW_MODES],
|
|
2663
|
+
},
|
|
2573
2664
|
...tierItems,
|
|
2574
2665
|
...fallbackItems,
|
|
2575
2666
|
...thinkingItems,
|
|
@@ -2628,6 +2719,7 @@ async function showConfigMenu(
|
|
|
2628
2719
|
|
|
2629
2720
|
let settingsList: SettingsList;
|
|
2630
2721
|
const refresh = () => {
|
|
2722
|
+
settingsList.updateValue("default_review_mode", draft.defaultReviewMode);
|
|
2631
2723
|
for (const tier of TIERS) {
|
|
2632
2724
|
settingsList.updateValue(tier, draft.tiers[tier] ?? UNSET);
|
|
2633
2725
|
settingsList.updateValue(`${tier}_fallbacks`, draft.fallbacks[tier]?.join(",") || "(none)");
|
|
@@ -2641,7 +2733,13 @@ async function showConfigMenu(
|
|
|
2641
2733
|
};
|
|
2642
2734
|
|
|
2643
2735
|
const persist = (id: string, newValue: string) => {
|
|
2644
|
-
if (
|
|
2736
|
+
if (id === "default_review_mode") {
|
|
2737
|
+
if (!REVIEW_MODES.includes(newValue as ReviewMode)) {
|
|
2738
|
+
ctx.ui.notify(`Invalid review mode: ${newValue}`, "error");
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2741
|
+
draft.defaultReviewMode = newValue as ReviewMode;
|
|
2742
|
+
} else if ((TIERS as string[]).includes(id)) {
|
|
2645
2743
|
if (newValue === "__unset__") delete draft.tiers[id as Tier];
|
|
2646
2744
|
else draft.tiers[id as Tier] = newValue;
|
|
2647
2745
|
} else if (isFallbackKey(id)) {
|
|
@@ -2689,7 +2787,9 @@ async function showConfigMenu(
|
|
|
2689
2787
|
try {
|
|
2690
2788
|
writeUserConfig(draft);
|
|
2691
2789
|
refresh();
|
|
2692
|
-
const shown = id === "
|
|
2790
|
+
const shown = id === "default_review_mode"
|
|
2791
|
+
? draft.defaultReviewMode
|
|
2792
|
+
: id === "tools"
|
|
2693
2793
|
? draft.tools.join(",")
|
|
2694
2794
|
: id === "auto_post_reviews"
|
|
2695
2795
|
? String(draft.autoPostReviews)
|
|
@@ -2706,7 +2806,17 @@ async function showConfigMenu(
|
|
|
2706
2806
|
: isToolPolicyKey(id)
|
|
2707
2807
|
? (draft.toolPolicies[tierFromCompoundKey(id)] ?? INHERIT_TOOL_POLICY)
|
|
2708
2808
|
: (draft.tiers[id as Tier] ?? UNSET);
|
|
2709
|
-
if (id === "
|
|
2809
|
+
if (id === "default_review_mode") {
|
|
2810
|
+
const effective = resolveDefaultReviewModeForContext(ctx);
|
|
2811
|
+
if (effective.source === "project") {
|
|
2812
|
+
ctx.ui.notify(
|
|
2813
|
+
`User defaultReviewMode saved as ${shown}, but trusted project config remains effective at ${effective.value}. Edit ${projectConfigPath(ctx.cwd)}.`,
|
|
2814
|
+
"warning",
|
|
2815
|
+
);
|
|
2816
|
+
} else {
|
|
2817
|
+
ctx.ui.notify(`PR review config: ${id} = ${shown} (effective ${effective.value})`, "info");
|
|
2818
|
+
}
|
|
2819
|
+
} else if (id === "auto_post_reviews") {
|
|
2710
2820
|
const effective = resolveAutoPostForContext(ctx);
|
|
2711
2821
|
if (effective.source === "project") {
|
|
2712
2822
|
ctx.ui.notify(
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
resolveAutoPostSetting,
|
|
40
40
|
canonicalReviewSnapshot,
|
|
41
41
|
resolveApproveMaxPriorityLevelSetting,
|
|
42
|
+
resolveDefaultReviewModeSetting,
|
|
42
43
|
type PublishResult,
|
|
43
44
|
resolveRepositoryBinding,
|
|
44
45
|
resolveReviewHostBinding,
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
type CompletedReviewRecord,
|
|
51
52
|
type CompletedReviewSessionIdentity,
|
|
52
53
|
type ReviewInvocation,
|
|
54
|
+
type ReviewModeResolution,
|
|
53
55
|
} from "../lib/pr-review-publish.ts";
|
|
54
56
|
import { demoteHeadings, mergeExtractedFindings, safeReviewBody, synthesizeReviewArtifact, type ReviewSynthesisArtifact } from "../lib/pr-review-markdown.ts";
|
|
55
57
|
import { resolveReviewDeadlinesForContext } from "../lib/pr-review-deadline-config.ts";
|
|
@@ -373,6 +375,7 @@ function readJsonObject(filePath: string): ConfigReadResult {
|
|
|
373
375
|
}
|
|
374
376
|
|
|
375
377
|
interface PublishingConfigResolution {
|
|
378
|
+
defaultReviewMode: ReviewModeResolution;
|
|
376
379
|
autoPost: AutoPostResolution;
|
|
377
380
|
allowStale: AutoPostResolution;
|
|
378
381
|
allowStaleApprovals: AutoPostResolution;
|
|
@@ -382,7 +385,8 @@ interface PublishingConfigResolution {
|
|
|
382
385
|
function invalidPublishingConfig(source: "user" | "project", error: string): PublishingConfigResolution {
|
|
383
386
|
const invalid = { value: false, valid: false, source, error } as const;
|
|
384
387
|
const invalidLevel = { value: "off" as const, valid: false, source, error } as const;
|
|
385
|
-
|
|
388
|
+
const invalidMode = { value: "balanced" as const, valid: false, source, error } as const;
|
|
389
|
+
return { defaultReviewMode: invalidMode, autoPost: invalid, allowStale: invalid, allowStaleApprovals: invalid, approveMaxPriority: invalidLevel };
|
|
386
390
|
}
|
|
387
391
|
|
|
388
392
|
function resolvePublishingConfig(ctx: ExtensionContext): PublishingConfigResolution {
|
|
@@ -398,6 +402,7 @@ function resolvePublishingConfig(ctx: ExtensionContext): PublishingConfigResolut
|
|
|
398
402
|
/* user config only */
|
|
399
403
|
}
|
|
400
404
|
return {
|
|
405
|
+
defaultReviewMode: resolveDefaultReviewModeSetting(user.value, project?.value),
|
|
401
406
|
autoPost: resolveAutoPostSetting(user.value, project?.value),
|
|
402
407
|
allowStale: resolveAllowStalePublishSetting(user.value, project?.value),
|
|
403
408
|
allowStaleApprovals: resolveAllowStaleApprovalsSetting(user.value, project?.value),
|
|
@@ -911,8 +916,18 @@ export default function registerReviewTable(
|
|
|
911
916
|
return { action: "handled" as const };
|
|
912
917
|
}
|
|
913
918
|
|
|
914
|
-
// Freeze trusted publication config and target binding before review tools or optional PR code can run.
|
|
919
|
+
// Freeze trusted mode/publication config and target binding before review tools or optional PR code can run.
|
|
915
920
|
const publishingConfig = resolvePublishingConfig(ctx);
|
|
921
|
+
const explicitReviewMode = parsed.reviewMode;
|
|
922
|
+
if (explicitReviewMode === undefined && !publishingConfig.defaultReviewMode.valid) {
|
|
923
|
+
ctx.ui.notify(`Invalid /pr-review invocation: ${publishingConfig.defaultReviewMode.error}`, "error");
|
|
924
|
+
return { action: "handled" as const };
|
|
925
|
+
}
|
|
926
|
+
const resolvedReviewMode = explicitReviewMode ?? publishingConfig.defaultReviewMode.value;
|
|
927
|
+
const resolvedParsed = { ...parsed, reviewMode: resolvedReviewMode };
|
|
928
|
+
const transformedInput = explicitReviewMode === undefined
|
|
929
|
+
? `${event.text.trimEnd()} --${resolvedReviewMode}`
|
|
930
|
+
: event.text;
|
|
916
931
|
const deadlineResolution = resolveReviewDeadlinesForContext(ctx);
|
|
917
932
|
const budget = createReviewBudget(deadlineResolution);
|
|
918
933
|
const preflight = {
|
|
@@ -952,7 +967,7 @@ export default function registerReviewTable(
|
|
|
952
967
|
}
|
|
953
968
|
activePreflight = undefined;
|
|
954
969
|
const gate = loopCoordinator.begin(
|
|
955
|
-
|
|
970
|
+
resolvedParsed,
|
|
956
971
|
publishingConfig.autoPost,
|
|
957
972
|
source,
|
|
958
973
|
ctx,
|
|
@@ -969,6 +984,9 @@ export default function registerReviewTable(
|
|
|
969
984
|
persistTelemetry("cleared");
|
|
970
985
|
return { action: "handled" as const };
|
|
971
986
|
}
|
|
987
|
+
if (transformedInput !== event.text) {
|
|
988
|
+
return { action: "transform" as const, text: transformedInput, images: event.images };
|
|
989
|
+
}
|
|
972
990
|
});
|
|
973
991
|
|
|
974
992
|
pi.on("turn_start", (_event, ctx) => {
|
|
@@ -29,6 +29,12 @@ export interface ReviewLaneAttemptArtifact {
|
|
|
29
29
|
readonly deadlineMs?: number;
|
|
30
30
|
/** Configured tier/fallback cap before batch/total truncation. */
|
|
31
31
|
readonly configuredDeadlineMs?: number;
|
|
32
|
+
/** Invocation time already consumed before this attempt was scheduled. */
|
|
33
|
+
readonly budgetElapsedBeforeAttemptMs?: number;
|
|
34
|
+
/** Remaining reviewer-batch window at scheduling time; negative means dispatch was already late. */
|
|
35
|
+
readonly batchRemainingBeforeAttemptMs?: number;
|
|
36
|
+
/** Remaining total invocation window at scheduling time. */
|
|
37
|
+
readonly totalRemainingBeforeAttemptMs?: number;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
export interface ExpectedReviewLane {
|
|
@@ -676,6 +682,9 @@ function attemptArtifactSnapshot(value: unknown): ReviewLaneAttemptArtifact | un
|
|
|
676
682
|
const forcedTermination = source.forcedTermination;
|
|
677
683
|
const deadlineMs = source.deadlineMs;
|
|
678
684
|
const configuredDeadlineMs = source.configuredDeadlineMs;
|
|
685
|
+
const budgetElapsedBeforeAttemptMs = source.budgetElapsedBeforeAttemptMs;
|
|
686
|
+
const batchRemainingBeforeAttemptMs = source.batchRemainingBeforeAttemptMs;
|
|
687
|
+
const totalRemainingBeforeAttemptMs = source.totalRemainingBeforeAttemptMs;
|
|
679
688
|
// --- Validation phase (locals only). ---
|
|
680
689
|
if (!isInteger(ordinal)) return undefined;
|
|
681
690
|
if (!isString(rawText)) return undefined;
|
|
@@ -700,6 +709,9 @@ function attemptArtifactSnapshot(value: unknown): ReviewLaneAttemptArtifact | un
|
|
|
700
709
|
if (!optional(forcedTermination, isBoolean)) return undefined;
|
|
701
710
|
if (!optional(deadlineMs, isFiniteNumber)) return undefined;
|
|
702
711
|
if (!optional(configuredDeadlineMs, isFiniteNumber)) return undefined;
|
|
712
|
+
if (!optional(budgetElapsedBeforeAttemptMs, isFiniteNumber)) return undefined;
|
|
713
|
+
if (!optional(batchRemainingBeforeAttemptMs, isFiniteNumber)) return undefined;
|
|
714
|
+
if (!optional(totalRemainingBeforeAttemptMs, isFiniteNumber)) return undefined;
|
|
703
715
|
// --- Snapshot phase. ---
|
|
704
716
|
const snapshot: Record<string, unknown> = {
|
|
705
717
|
ordinal,
|
|
@@ -726,6 +738,9 @@ function attemptArtifactSnapshot(value: unknown): ReviewLaneAttemptArtifact | un
|
|
|
726
738
|
withOptional(snapshot, "forcedTermination", forcedTermination);
|
|
727
739
|
withOptional(snapshot, "deadlineMs", deadlineMs);
|
|
728
740
|
withOptional(snapshot, "configuredDeadlineMs", configuredDeadlineMs);
|
|
741
|
+
withOptional(snapshot, "budgetElapsedBeforeAttemptMs", budgetElapsedBeforeAttemptMs);
|
|
742
|
+
withOptional(snapshot, "batchRemainingBeforeAttemptMs", batchRemainingBeforeAttemptMs);
|
|
743
|
+
withOptional(snapshot, "totalRemainingBeforeAttemptMs", totalRemainingBeforeAttemptMs);
|
|
729
744
|
return Object.freeze(snapshot) as ReviewLaneAttemptArtifact;
|
|
730
745
|
} catch {
|
|
731
746
|
// A throwing getter is a malformed attempt, never a crash at this boundary.
|
|
@@ -105,6 +105,8 @@ export function resolveReviewDeadlines(user: unknown, project?: unknown): Deadli
|
|
|
105
105
|
|
|
106
106
|
export interface ReviewBudget {
|
|
107
107
|
readonly startedAtMs: number;
|
|
108
|
+
/** First reviewer dispatch time. Undefined until the batch window is activated. */
|
|
109
|
+
readonly batchStartedAtMs?: number;
|
|
108
110
|
readonly totalDeadlineMs: number;
|
|
109
111
|
readonly batchDeadlineMs: number;
|
|
110
112
|
readonly config: ReviewDeadlineConfig;
|
|
@@ -130,6 +132,37 @@ export function createReviewBudget(
|
|
|
130
132
|
});
|
|
131
133
|
}
|
|
132
134
|
|
|
135
|
+
const activatedBatchBudgets = new WeakMap<ReviewBudget, ReviewBudget>();
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Start the reviewer-batch window once, at the first reviewer dispatch.
|
|
139
|
+
*
|
|
140
|
+
* PR binding, diff capture, and parent orchestration remain charged to the total
|
|
141
|
+
* invocation deadline, but no longer consume `batchMs` before a reviewer exists.
|
|
142
|
+
* The original synthesis and termination reserves remain hard upper bounds.
|
|
143
|
+
*/
|
|
144
|
+
export function activateReviewBatch(
|
|
145
|
+
budget: ReviewBudget,
|
|
146
|
+
now: MonotonicNow = monotonicNow,
|
|
147
|
+
): ReviewBudget {
|
|
148
|
+
if (budget.batchStartedAtMs !== undefined) return budget;
|
|
149
|
+
const existing = activatedBatchBudgets.get(budget);
|
|
150
|
+
if (existing) return existing;
|
|
151
|
+
const batchStartedAtMs = now();
|
|
152
|
+
const terminationReserveMs = budget.config.terminationGraceMs + budget.config.cleanupReserveMs;
|
|
153
|
+
const batchDeadlineMs = Math.min(
|
|
154
|
+
batchStartedAtMs + budget.config.batchMs,
|
|
155
|
+
budget.totalDeadlineMs - budget.config.synthesisMs - terminationReserveMs,
|
|
156
|
+
);
|
|
157
|
+
const activated = Object.freeze({
|
|
158
|
+
...budget,
|
|
159
|
+
batchStartedAtMs,
|
|
160
|
+
batchDeadlineMs,
|
|
161
|
+
});
|
|
162
|
+
activatedBatchBudgets.set(budget, activated);
|
|
163
|
+
return activated;
|
|
164
|
+
}
|
|
165
|
+
|
|
133
166
|
export function attemptDeadline(
|
|
134
167
|
budget: ReviewBudget,
|
|
135
168
|
tier: ReviewTier,
|
package/lib/pr-review-publish.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { monotonicNow, type MonotonicNow } from "./pr-review-telemetry.ts";
|
|
|
7
7
|
|
|
8
8
|
export type PublishMode = "auto" | "force" | "disabled";
|
|
9
9
|
export type ReviewMode = "quick" | "balanced" | "full" | "deep";
|
|
10
|
+
export const REVIEW_MODES: readonly ReviewMode[] = ["quick", "balanced", "full", "deep"];
|
|
10
11
|
export type AutoPostSource = "default" | "user" | "project";
|
|
11
12
|
export type CompletionAction = "continue_tools" | "accept_final" | "clear_invocation";
|
|
12
13
|
|
|
@@ -30,6 +31,47 @@ export interface ApproveMaxPriorityLevelResolution {
|
|
|
30
31
|
readonly error?: string;
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
export interface ReviewModeResolution {
|
|
35
|
+
readonly value: ReviewMode;
|
|
36
|
+
readonly valid: boolean;
|
|
37
|
+
readonly source: AutoPostSource;
|
|
38
|
+
readonly error?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isReviewMode(value: unknown): value is ReviewMode {
|
|
42
|
+
return typeof value === "string" && REVIEW_MODES.includes(value as ReviewMode);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Resolve the default reviewer topology, with a trusted project overlaying user config. */
|
|
46
|
+
export function resolveDefaultReviewModeSetting(
|
|
47
|
+
user: unknown,
|
|
48
|
+
trustedProject?: unknown,
|
|
49
|
+
): ReviewModeResolution {
|
|
50
|
+
if (hasOwn(trustedProject, "defaultReviewMode")) {
|
|
51
|
+
const value = (trustedProject as { defaultReviewMode?: unknown }).defaultReviewMode;
|
|
52
|
+
return isReviewMode(value)
|
|
53
|
+
? { value, valid: true, source: "project" }
|
|
54
|
+
: {
|
|
55
|
+
value: "balanced",
|
|
56
|
+
valid: false,
|
|
57
|
+
source: "project",
|
|
58
|
+
error: `project defaultReviewMode must be one of: ${REVIEW_MODES.join(", ")}`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (hasOwn(user, "defaultReviewMode")) {
|
|
62
|
+
const value = (user as { defaultReviewMode?: unknown }).defaultReviewMode;
|
|
63
|
+
return isReviewMode(value)
|
|
64
|
+
? { value, valid: true, source: "user" }
|
|
65
|
+
: {
|
|
66
|
+
value: "balanced",
|
|
67
|
+
valid: false,
|
|
68
|
+
source: "user",
|
|
69
|
+
error: `user defaultReviewMode must be one of: ${REVIEW_MODES.join(", ")}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return { value: "balanced", valid: true, source: "default" };
|
|
73
|
+
}
|
|
74
|
+
|
|
33
75
|
function isValidApproveLevel(value: unknown): value is ApproveMaxPriorityLevel {
|
|
34
76
|
return value === "off" || (typeof value === "string" && APPROVE_PRIORITY_LEVELS.includes(value as (typeof APPROVE_PRIORITY_LEVELS)[number]));
|
|
35
77
|
}
|
|
@@ -233,7 +275,15 @@ export function parsePublishMode(input: string): PublishModeParseResult {
|
|
|
233
275
|
return {
|
|
234
276
|
matched: true,
|
|
235
277
|
mode: disabled ? "disabled" : force ? "force" : "auto",
|
|
236
|
-
|
|
278
|
+
...(deep
|
|
279
|
+
? { reviewMode: "deep" as const }
|
|
280
|
+
: full
|
|
281
|
+
? { reviewMode: "full" as const }
|
|
282
|
+
: quick || majorOnly
|
|
283
|
+
? { reviewMode: "quick" as const }
|
|
284
|
+
: balanced
|
|
285
|
+
? { reviewMode: "balanced" as const }
|
|
286
|
+
: {}),
|
|
237
287
|
prNumber: requested,
|
|
238
288
|
allowNonOpen: tokens.includes("--include-closed") || tokens.includes("--review-closed"),
|
|
239
289
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pr-review",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.1",
|
|
4
4
|
"description": "Parallel AI code review for GitHub pull requests in the Pi coding agent, with model-agnostic tiered subagents, structured findings, optional verification, and host-gated COMMENT or qualified APPROVE publishing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/prompts/pr-review.md
CHANGED
|
@@ -54,7 +54,7 @@ Arguments for this run: `$@`
|
|
|
54
54
|
- `$1` is the PR number (required).
|
|
55
55
|
- `--comment` explicitly requests one GitHub review for this run, even when automatic posting is disabled. A fully parsed, complete Markdown review may qualify for host-gated `APPROVE`; degraded Markdown remains `COMMENT`, and assistant text never directly selects the GitHub event.
|
|
56
56
|
- `--no-comment` suppresses posting for this run, even when automatic posting is enabled.
|
|
57
|
-
- With no review-mode flag,
|
|
57
|
+
- With no review-mode flag in the user's invocation, the host appends the configured default mode before expanding this prompt. The installation default is balanced.
|
|
58
58
|
- `--quick` selects three P0–P2 reviewers; `--major-only` is its compatibility alias.
|
|
59
59
|
- `--balanced` explicitly selects the five-reviewer default.
|
|
60
60
|
- `--full` selects six all-severity reviewers, including conventions/maintainability.
|