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 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<{ result: RunResult; notice: string; elapsedMs: number; deadlineMs?: number }> {
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("Review attempt timed out before it could start within the batch deadline.");
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 { result, notice: noticeForAttempt(pass.tier, attempt), elapsedMs: monotonicNow() - startedAt, deadlineMs };
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 { result, notice, elapsedMs, deadlineMs } = await runSubagentAttempt(
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
- lease.budget,
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
- lease.budget,
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-tier settings, or show/set models, thinking, and fallbacks for /pr-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=... heavy_fallbacks=...`
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 ((TIERS as string[]).includes(key)) {
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 directly: `/pr-review-config light=provider/model heavy=provider/model:high`",
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 ((TIERS as string[]).includes(id)) {
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 === "tools"
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 === "auto_post_reviews") {
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
- return { autoPost: invalid, allowStale: invalid, allowStaleApprovals: invalid, approveMaxPriority: invalidLevel };
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
- parsed,
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,
@@ -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
- reviewMode: deep ? "deep" : full ? "full" : quick || majorOnly ? "quick" : "balanced",
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.16.0",
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",
@@ -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, use the balanced five-reviewer default.
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.