pi-pr-review 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.17.0](https://github.com/10ego/pi-pr-review/compare/v1.16.0...v1.17.0) (2026-08-29)
4
+
5
+
6
+ ### Features
7
+
8
+ * **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))
9
+
3
10
  ## [1.16.0](https://github.com/10ego/pi-pr-review/compare/v1.15.16...v1.16.0) (2026-08-28)
4
11
 
5
12
 
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",
@@ -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"),
@@ -2072,7 +2091,7 @@ export default function registerPrReviewSubagents(
2072
2091
  });
2073
2092
 
2074
2093
  pi.registerCommand("pr-review-config", {
2075
- description: "Open review-tier settings, or show/set models, thinking, and fallbacks for /pr-review",
2094
+ description: "Open review settings, or set the default mode, models, thinking, and publication policy",
2076
2095
  handler: async (args, ctx) => {
2077
2096
  // Extension commands execute before input events, so revoke explicitly.
2078
2097
  loopCoordinator.clear();
@@ -2085,7 +2104,7 @@ export default function registerPrReviewSubagents(
2085
2104
  }
2086
2105
 
2087
2106
  try {
2088
- // Direct set: `/pr-review-config light=... heavy=... heavy_fallbacks=...`
2107
+ // Direct set: `/pr-review-config default_review_mode=full light=... heavy=...`
2089
2108
  if (parsed.hasChanges) {
2090
2109
  const next = applyConfigPatch(readUserConfig(), parsed.patch);
2091
2110
  writeUserConfig(next);
@@ -2145,6 +2164,7 @@ function shouldOpenConfigMenu(args: string, ctx: ExtensionContext): boolean {
2145
2164
  }
2146
2165
 
2147
2166
  const CONFIG_COMPLETIONS: Array<{ value: string; label: string }> = [
2167
+ { value: "default_review_mode=", label: "default_review_mode=<quick|balanced|full|deep> — topology used when no mode flag is supplied" },
2148
2168
  ...TIERS.map((t) => ({ value: `${t}=`, label: `${t}=<model> — ${TIER_PURPOSE[t]}` })),
2149
2169
  ...TIERS.map((t) => ({
2150
2170
  value: `${t}_fallbacks=`,
@@ -2233,6 +2253,7 @@ function tierFromCompoundKey(key: string): Tier {
2233
2253
  }
2234
2254
 
2235
2255
  interface ConfigPatch {
2256
+ defaultReviewMode?: ReviewMode;
2236
2257
  tiers: Partial<Record<Tier, string | null>>;
2237
2258
  fallbacks: Partial<Record<Tier, string[] | null>>;
2238
2259
  thinkingLevels: Partial<Record<Tier, ThinkingLevel | null>>;
@@ -2256,7 +2277,11 @@ function parseConfigArgs(args: string): { patch: ConfigPatch; hasChanges: boolea
2256
2277
  }
2257
2278
  const key = token.slice(0, eq).replace(/^--?/, "").toLowerCase();
2258
2279
  const value = token.slice(eq + 1);
2259
- if ((TIERS as string[]).includes(key)) {
2280
+ if (key === "default_review_mode" || key === "defaultreviewmode" || key === "default-review-mode") {
2281
+ const normalized = value.toLowerCase();
2282
+ if (REVIEW_MODES.includes(normalized as ReviewMode)) patch.defaultReviewMode = normalized as ReviewMode;
2283
+ else errors.push(`invalid ${key} "${value}" (expected ${REVIEW_MODES.join("|")})`);
2284
+ } else if ((TIERS as string[]).includes(key)) {
2260
2285
  patch.tiers[key as Tier] = value === "" || value === "unset" ? null : value;
2261
2286
  } else if (isFallbackKey(key)) {
2262
2287
  const tier = tierFromCompoundKey(key);
@@ -2308,11 +2333,12 @@ function parseConfigArgs(args: string): { patch: ConfigPatch; hasChanges: boolea
2308
2333
  patch.tools = splitCommaList(value);
2309
2334
  } else {
2310
2335
  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)`,
2336
+ `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
2337
  );
2313
2338
  }
2314
2339
  }
2315
2340
  const hasChanges =
2341
+ patch.defaultReviewMode !== undefined ||
2316
2342
  Object.keys(patch.tiers).length > 0 ||
2317
2343
  Object.keys(patch.fallbacks).length > 0 ||
2318
2344
  Object.keys(patch.thinkingLevels).length > 0 ||
@@ -2327,6 +2353,7 @@ function parseConfigArgs(args: string): { patch: ConfigPatch; hasChanges: boolea
2327
2353
 
2328
2354
  function applyConfigPatch(base: PrReviewConfig, patch: ConfigPatch): PrReviewConfig {
2329
2355
  const next: PrReviewConfig = {
2356
+ defaultReviewMode: base.defaultReviewMode,
2330
2357
  tiers: { ...base.tiers },
2331
2358
  fallbacks: normalizeFallbacks(base.fallbacks),
2332
2359
  thinkingLevels: normalizeThinkingLevels(base.thinkingLevels, "PR review config"),
@@ -2339,6 +2366,7 @@ function applyConfigPatch(base: PrReviewConfig, patch: ConfigPatch): PrReviewCon
2339
2366
  tools: [...base.tools],
2340
2367
  deadlines: base.deadlines,
2341
2368
  };
2369
+ if (patch.defaultReviewMode !== undefined) next.defaultReviewMode = patch.defaultReviewMode;
2342
2370
  for (const tier of TIERS) {
2343
2371
  if (tier in patch.tiers) {
2344
2372
  const value = patch.tiers[tier];
@@ -2388,6 +2416,7 @@ function summarizeConfig(
2388
2416
  } catch {
2389
2417
  /* ignore */
2390
2418
  }
2419
+ const defaultReviewMode = resolveDefaultReviewModeForContext(ctx);
2391
2420
  const autoPost = resolveAutoPostForContext(ctx);
2392
2421
  const allowStale = resolveAllowStaleForContext(ctx);
2393
2422
  const allowStaleApprovals = resolveAllowStaleApprovalsForContext(ctx);
@@ -2401,6 +2430,7 @@ function summarizeConfig(
2401
2430
  (t) =>
2402
2431
  `| \`${t}\` | ${user.tiers[t] ? `\`${user.tiers[t]}\`` : "_unset_"} | ${effective.tiers[t] ? `\`${effective.tiers[t]}\`` : "_pi default_"} | ${TIER_PURPOSE[t]} |`,
2403
2432
  ),
2433
+ `| \`defaultReviewMode\` | \`${user.defaultReviewMode}\` | \`${effective.defaultReviewMode}\` (${defaultReviewMode.source}) | topology used when no explicit mode flag is supplied; default \`balanced\` |`,
2404
2434
  `| \`autoPostReviews\` | \`${user.autoPostReviews}\` | \`${effective.autoPostReviews}\` (${autoPost.source}) | automatically post one GitHub review; default \`false\` |`,
2405
2435
  `| \`allowStalePublish\` | \`${user.allowStalePublish}\` | \`${effective.allowStalePublish}\` (${allowStale.source}) | permit body-only stale publication with reviewed/current SHAs; default \`true\` |`,
2406
2436
  `| \`allowStaleApprovals\` | \`${user.allowStaleApprovals}\` | \`${effective.allowStaleApprovals}\` (${allowStaleApprovals.source}) | permit qualified stale fully parsed reviews to record APPROVE; default \`false\` |`,
@@ -2420,6 +2450,10 @@ function summarizeConfig(
2420
2450
  if (projectPath) lines.push(`Project overlay (trusted): \`${projectPath}\``);
2421
2451
  const warnings = thinkingWarnings(effective);
2422
2452
  if (warnings.length) lines.push("", ...warnings);
2453
+ if (!defaultReviewMode.valid) lines.push(`Default review mode config error: ${defaultReviewMode.error}`);
2454
+ else if (defaultReviewMode.source === "project") {
2455
+ lines.push("Default review mode is controlled by the trusted project overlay; this command edits user config only.");
2456
+ }
2423
2457
  if (!autoPost.valid) lines.push(`Automatic posting config error: ${autoPost.error}`);
2424
2458
  else if (autoPost.source === "project") {
2425
2459
  lines.push("Automatic posting is controlled by the trusted project overlay; this command edits user config only.");
@@ -2441,7 +2475,8 @@ function summarizeConfig(
2441
2475
  "## Usage",
2442
2476
  "- Open the settings menu: `/pr-review-config`",
2443
2477
  "- Print this summary: `/pr-review-config show`",
2444
- "- Set directly: `/pr-review-config light=provider/model heavy=provider/model:high`",
2478
+ "- Set the no-flag review topology: `/pr-review-config default_review_mode=balanced`",
2479
+ "- Set models directly: `/pr-review-config light=provider/model heavy=provider/model:high`",
2445
2480
  "- Set fallback chain: `/pr-review-config heavy_fallbacks=provider/backup:high,provider/backup2`",
2446
2481
  "- Set tier thinking: `/pr-review-config light_thinking=low medium_thinking=medium heavy_thinking=high`",
2447
2482
  "- Enable automatic GitHub review posting: `/pr-review-config auto_post_reviews=true`",
@@ -2570,6 +2605,13 @@ function configMenuItems(cfg: PrReviewConfig, available: string[]): SettingItem[
2570
2605
  const current = cfg.tools.join(",");
2571
2606
  const toolValues = [current, ...TOOLS_PRESETS.filter((p) => p !== current)];
2572
2607
  return [
2608
+ {
2609
+ id: "default_review_mode",
2610
+ label: "default review mode",
2611
+ description: "Topology used when /pr-review has no explicit mode flag. Enter/Space cycles values.",
2612
+ currentValue: cfg.defaultReviewMode,
2613
+ values: [...REVIEW_MODES],
2614
+ },
2573
2615
  ...tierItems,
2574
2616
  ...fallbackItems,
2575
2617
  ...thinkingItems,
@@ -2628,6 +2670,7 @@ async function showConfigMenu(
2628
2670
 
2629
2671
  let settingsList: SettingsList;
2630
2672
  const refresh = () => {
2673
+ settingsList.updateValue("default_review_mode", draft.defaultReviewMode);
2631
2674
  for (const tier of TIERS) {
2632
2675
  settingsList.updateValue(tier, draft.tiers[tier] ?? UNSET);
2633
2676
  settingsList.updateValue(`${tier}_fallbacks`, draft.fallbacks[tier]?.join(",") || "(none)");
@@ -2641,7 +2684,13 @@ async function showConfigMenu(
2641
2684
  };
2642
2685
 
2643
2686
  const persist = (id: string, newValue: string) => {
2644
- if ((TIERS as string[]).includes(id)) {
2687
+ if (id === "default_review_mode") {
2688
+ if (!REVIEW_MODES.includes(newValue as ReviewMode)) {
2689
+ ctx.ui.notify(`Invalid review mode: ${newValue}`, "error");
2690
+ return;
2691
+ }
2692
+ draft.defaultReviewMode = newValue as ReviewMode;
2693
+ } else if ((TIERS as string[]).includes(id)) {
2645
2694
  if (newValue === "__unset__") delete draft.tiers[id as Tier];
2646
2695
  else draft.tiers[id as Tier] = newValue;
2647
2696
  } else if (isFallbackKey(id)) {
@@ -2689,7 +2738,9 @@ async function showConfigMenu(
2689
2738
  try {
2690
2739
  writeUserConfig(draft);
2691
2740
  refresh();
2692
- const shown = id === "tools"
2741
+ const shown = id === "default_review_mode"
2742
+ ? draft.defaultReviewMode
2743
+ : id === "tools"
2693
2744
  ? draft.tools.join(",")
2694
2745
  : id === "auto_post_reviews"
2695
2746
  ? String(draft.autoPostReviews)
@@ -2706,7 +2757,17 @@ async function showConfigMenu(
2706
2757
  : isToolPolicyKey(id)
2707
2758
  ? (draft.toolPolicies[tierFromCompoundKey(id)] ?? INHERIT_TOOL_POLICY)
2708
2759
  : (draft.tiers[id as Tier] ?? UNSET);
2709
- if (id === "auto_post_reviews") {
2760
+ if (id === "default_review_mode") {
2761
+ const effective = resolveDefaultReviewModeForContext(ctx);
2762
+ if (effective.source === "project") {
2763
+ ctx.ui.notify(
2764
+ `User defaultReviewMode saved as ${shown}, but trusted project config remains effective at ${effective.value}. Edit ${projectConfigPath(ctx.cwd)}.`,
2765
+ "warning",
2766
+ );
2767
+ } else {
2768
+ ctx.ui.notify(`PR review config: ${id} = ${shown} (effective ${effective.value})`, "info");
2769
+ }
2770
+ } else if (id === "auto_post_reviews") {
2710
2771
  const effective = resolveAutoPostForContext(ctx);
2711
2772
  if (effective.source === "project") {
2712
2773
  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) => {
@@ -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.0",
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.