@zephyrdeng/pi-review 0.9.1 → 0.11.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.
Files changed (68) hide show
  1. package/README.md +29 -3
  2. package/README.zh-CN.md +9 -4
  3. package/dist/cli-exit.test.js +19 -1
  4. package/dist/cli-exit.test.js.map +1 -1
  5. package/dist/index.d.ts +2 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/json-events.d.ts +1 -0
  10. package/dist/json-events.d.ts.map +1 -1
  11. package/dist/json-events.js +12 -2
  12. package/dist/json-events.js.map +1 -1
  13. package/dist/json-events.test.js +22 -0
  14. package/dist/json-events.test.js.map +1 -1
  15. package/dist/loop.test.js +2 -0
  16. package/dist/loop.test.js.map +1 -1
  17. package/dist/meta-footer.d.ts.map +1 -1
  18. package/dist/meta-footer.js +6 -1
  19. package/dist/meta-footer.js.map +1 -1
  20. package/dist/meta-footer.test.js +85 -1
  21. package/dist/meta-footer.test.js.map +1 -1
  22. package/dist/panel-aggregate.d.ts +7 -0
  23. package/dist/panel-aggregate.d.ts.map +1 -1
  24. package/dist/panel-aggregate.js +22 -1
  25. package/dist/panel-aggregate.js.map +1 -1
  26. package/dist/panel-aggregate.test.js +110 -1
  27. package/dist/panel-aggregate.test.js.map +1 -1
  28. package/dist/panel-cli.test.js +69 -1
  29. package/dist/panel-cli.test.js.map +1 -1
  30. package/dist/panel-loop.test.js +2 -0
  31. package/dist/panel-loop.test.js.map +1 -1
  32. package/dist/panel-view.d.ts.map +1 -1
  33. package/dist/panel-view.js +9 -3
  34. package/dist/panel-view.js.map +1 -1
  35. package/dist/panel-view.test.js +62 -0
  36. package/dist/panel-view.test.js.map +1 -1
  37. package/dist/panel.d.ts.map +1 -1
  38. package/dist/panel.js +11 -5
  39. package/dist/panel.js.map +1 -1
  40. package/dist/prompt.js +1 -1
  41. package/dist/prompt.js.map +1 -1
  42. package/dist/prompt.test.js +2 -0
  43. package/dist/prompt.test.js.map +1 -1
  44. package/dist/review-events.d.ts +1 -0
  45. package/dist/review-events.d.ts.map +1 -1
  46. package/dist/review-result.d.ts.map +1 -1
  47. package/dist/review-result.js +46 -1
  48. package/dist/review-result.js.map +1 -1
  49. package/dist/review-result.test.js +144 -0
  50. package/dist/review-result.test.js.map +1 -1
  51. package/dist/review.d.ts.map +1 -1
  52. package/dist/review.js +2 -0
  53. package/dist/review.js.map +1 -1
  54. package/dist/types.d.ts +56 -0
  55. package/dist/types.d.ts.map +1 -1
  56. package/dist/types.js +9 -0
  57. package/dist/types.js.map +1 -1
  58. package/dist/ui-client.test.js +2 -0
  59. package/dist/ui-client.test.js.map +1 -1
  60. package/extensions/panel-tool.test.ts +47 -0
  61. package/extensions/panel-tool.ts +3 -1
  62. package/extensions/rv-interactive.test.ts +179 -8
  63. package/extensions/rv-interactive.ts +64 -61
  64. package/package.json +1 -1
  65. package/resources/system-prompt.md +3 -1
  66. package/skills/pi-review/SKILL.md +7 -6
  67. package/skills/pi-review/references/codex-tools.md +3 -3
  68. package/skills/pi-review/references/model-selection.md +2 -2
@@ -152,6 +152,7 @@ test("Pi panel renderer expands final findings and reviewer provenance", () => {
152
152
  at: now,
153
153
  type: "panel.completed",
154
154
  meta: {
155
+ metaVersion: 1,
155
156
  strategy: "panel",
156
157
  reviewMode: "code",
157
158
  status: "has_findings",
@@ -191,6 +192,7 @@ test("Pi panel renderer expands final findings and reviewer provenance", () => {
191
192
  at: now,
192
193
  type: "panel.completed",
193
194
  meta: {
195
+ metaVersion: 1,
194
196
  strategy: "panel",
195
197
  reviewMode: "code",
196
198
  status: "clean",
@@ -227,6 +229,7 @@ test("Pi panel result content includes duration, token, and cost metrics", () =>
227
229
  at: Date.now(),
228
230
  type: "panel.completed",
229
231
  meta: {
232
+ metaVersion: 1,
230
233
  strategy: "panel",
231
234
  reviewMode: "code",
232
235
  status: "clean",
@@ -275,6 +278,49 @@ test("Pi panel result content includes duration, token, and cost metrics", () =>
275
278
  assert.match(content, /\$0\.0617/);
276
279
  });
277
280
 
281
+ test("panel tool reviewer summary falls back to the response model when a reviewer has no configured model", () => {
282
+ const content = buildPanelResultContent({
283
+ ...createPanelViewState(),
284
+ meta: {
285
+ metaVersion: 1,
286
+ strategy: "panel",
287
+ reviewMode: "code",
288
+ status: "clean",
289
+ verdict: "approve",
290
+ verdictSource: "parsed",
291
+ findings: [],
292
+ actionableCount: 0,
293
+ durationMs: 100,
294
+ model: "actual/reported-model",
295
+ configuredReviewers: 1,
296
+ successfulReviewers: 1,
297
+ consensusPolicy: "quorum",
298
+ consensusThreshold: 1,
299
+ panelHealth: "healthy",
300
+ confirmedClusters: [],
301
+ advisories: [],
302
+ reviewers: [
303
+ {
304
+ reviewerId: "r1",
305
+ role: "Independent reviewer",
306
+ model: null,
307
+ responseModel: "actual/reported-model",
308
+ durationMs: 90,
309
+ status: "clean",
310
+ verdict: "approve",
311
+ verdictSource: "parsed",
312
+ contributed: true,
313
+ },
314
+ ],
315
+ adjudicationUsed: false,
316
+ },
317
+ });
318
+ // Panel-level Model line (meta-footer.ts) and the reviewer summary line
319
+ // (panel-tool.ts fromMeta branch) both fall back to the response model.
320
+ assert.match(content, /Model\s+actual\/reported-model/);
321
+ assert.match(content, /- r1 · Independent reviewer · actual\/reported-model · clean · approve/);
322
+ });
323
+
278
324
  test("panel tool updates and clears ambient footer status during execute", async () => {
279
325
  const statuses: Array<string | undefined> = [];
280
326
  let registered: { execute: (...args: any[]) => Promise<any> } | undefined;
@@ -346,6 +392,7 @@ test("panel tool content carries full conclusion for the parent LLM", () => {
346
392
  at: now,
347
393
  type: "panel.completed",
348
394
  meta: {
395
+ metaVersion: 1,
349
396
  strategy: "panel",
350
397
  reviewMode: "code",
351
398
  status: "has_findings",
@@ -112,7 +112,9 @@ function reviewerSummaryLines(state: PanelViewState): string[] {
112
112
  return fromMeta.map((reviewer) => {
113
113
  const live = state.reviewers[reviewer.reviewerId];
114
114
  const role = reviewer.role ? ` · ${reviewer.role}` : "";
115
- const model = reviewer.model ? ` · ${reviewer.model}` : "";
115
+ // Effective model: configured model when set, else the provider-reported model.
116
+ const effectiveModel = reviewer.model ?? reviewer.responseModel;
117
+ const model = effectiveModel ? ` · ${effectiveModel}` : "";
116
118
  const thinking = reviewer.thinking ? ` · ${reviewer.thinking}` : "";
117
119
  const usage = reviewer.usage ? ` · ${formatTokens(reviewer.usage.totalTokens)} tok · ${formatUsage(reviewer.usage)}` : "";
118
120
  const cost = reviewer.usage ? ` · cost ${typeof reviewer.usage.costTotal === "number" ? formatCost(reviewer.usage.costTotal) : "n/a"}` : "";
@@ -63,22 +63,28 @@ function seed(over: Partial<RvParsed> = {}): RvParsed {
63
63
  };
64
64
  }
65
65
 
66
+ /** Push this into `selects` to simulate the user pressing Esc on that select step. */
67
+ const SELECT_CANCEL = "__cancel__";
68
+
66
69
  function scriptedUi(answers: {
67
70
  selects?: string[];
68
71
  inputs?: string[];
69
72
  confirms?: boolean[];
70
73
  customModelPickerResults?: (string | "__skip__" | undefined)[];
71
- }): InteractiveUi & { log: string[] } {
74
+ }): InteractiveUi & { log: string[]; confirmMessages: string[] } {
72
75
  const selects = [...(answers.selects ?? [])];
73
76
  const inputs = [...(answers.inputs ?? [])];
74
77
  const confirms = [...(answers.confirms ?? [])];
75
78
  const pickerResults = [...(answers.customModelPickerResults ?? [])];
76
79
  const log: string[] = [];
80
+ const confirmMessages: string[] = [];
77
81
  return {
78
82
  log,
83
+ confirmMessages,
79
84
  async select(title, options) {
80
85
  log.push(`select:${title} :: ${options.slice(0, 3).join(" | ")}`);
81
- return selects.shift() ?? options[0];
86
+ const next = selects.length > 0 ? selects.shift() : options[0];
87
+ return next === SELECT_CANCEL ? undefined : next;
82
88
  },
83
89
  async input(title) {
84
90
  log.push(`input:${title}`);
@@ -87,6 +93,7 @@ function scriptedUi(answers: {
87
93
  async confirm(title, message) {
88
94
  log.push(`confirm:${title}`);
89
95
  log.push(message.split("\n")[0] ?? "");
96
+ confirmMessages.push(message);
90
97
  return confirms.shift() ?? true;
91
98
  },
92
99
  notify(message) {
@@ -152,7 +159,7 @@ test("wizard assigns per-reviewer models through select dialogs", async () => {
152
159
  "1 · host fix point (recommended)",
153
160
  "Custom reviewer count 2–8 (r1..rN, pick models)",
154
161
  "3",
155
- "Pick separately for each reviewer",
162
+ "Pick a model for each reviewer",
156
163
  "openai-codex/gpt-5.6-sol",
157
164
  "xhigh",
158
165
  "anthropic/claude-sonnet-4-5",
@@ -162,7 +169,7 @@ test("wizard assigns per-reviewer models through select dialogs", async () => {
162
169
  "quorum · at least min-agree agree (default)",
163
170
  "2",
164
171
  ],
165
- confirms: [true, true],
172
+ confirms: [true],
166
173
  });
167
174
 
168
175
  const result = await runRvInteractiveWizard(ui, {
@@ -184,6 +191,9 @@ test("wizard assigns per-reviewer models through select dialogs", async () => {
184
191
  "r2=anthropic/claude-sonnet-4-5:high",
185
192
  "r3=openai-codex/gpt-5.6-sol:xhigh",
186
193
  ]);
194
+ // Confirm summary shows every reviewer's resolved model, not a raw reviewer-models dump.
195
+ const summary = ui.confirmMessages.at(-1) ?? "";
196
+ assert.match(summary, /reviewer models: r1=openai-codex\/gpt-5\.6-sol:xhigh, r2=anthropic\/claude-sonnet-4-5:high, r3=openai-codex\/gpt-5\.6-sol:xhigh/);
187
197
  });
188
198
 
189
199
  test("wizard keeps an explicit single-reviewer non-panel choice", async () => {
@@ -208,6 +218,9 @@ test("wizard keeps an explicit single-reviewer non-panel choice", async () => {
208
218
  assert.equal(result!.reviewers, 1);
209
219
  assert.equal(result!.panel, undefined);
210
220
  assert.equal(result!.consensus, undefined);
221
+ // Single-reviewer summary keeps the `model:` line and shows the Pi default placeholder.
222
+ const summary = ui.confirmMessages.at(-1) ?? "";
223
+ assert.match(summary, /model: Pi default/);
211
224
  });
212
225
 
213
226
  test("wizard can use code-experts preset and shared model", async () => {
@@ -224,7 +237,7 @@ test("wizard can use code-experts preset and shared model", async () => {
224
237
  "quorum · at least min-agree agree (default)",
225
238
  "2",
226
239
  ],
227
- confirms: [true, true],
240
+ confirms: [true],
228
241
  });
229
242
 
230
243
  const result = await runRvInteractiveWizard(ui, {
@@ -241,6 +254,62 @@ test("wizard can use code-experts preset and shared model", async () => {
241
254
  "security=openai-codex/gpt-5.6-sol:xhigh",
242
255
  "testing=openai-codex/gpt-5.6-sol:xhigh",
243
256
  ]);
257
+ const summary = ui.confirmMessages.at(-1) ?? "";
258
+ assert.match(summary, /reviewer models: correctness=openai-codex\/gpt-5\.6-sol:xhigh, security=openai-codex\/gpt-5\.6-sol:xhigh, testing=openai-codex\/gpt-5\.6-sol:xhigh/);
259
+ });
260
+
261
+ test("wizard can skip model assignment and use Pi's default model for every reviewer", async () => {
262
+ const ui = scriptedUi({
263
+ inputs: ["review auth"],
264
+ selects: [
265
+ "code review (code)",
266
+ "Single gate (fix then re-run /rv-loop) · recommended",
267
+ "1 · host fix point (recommended)",
268
+ "Preset code-experts (correctness/security/testing)",
269
+ "Use Pi's default model (resolved at runtime)",
270
+ "quorum · at least min-agree agree (default)",
271
+ "2",
272
+ ],
273
+ confirms: [true],
274
+ });
275
+
276
+ const result = await runRvInteractiveWizard(ui, {
277
+ strategy: "loop",
278
+ seed: seed(),
279
+ models,
280
+ locale: "en",
281
+ });
282
+
283
+ assert.ok(result);
284
+ assert.equal(result!.panel, "code-experts");
285
+ // No per-reviewer overrides and no shared --model: every reviewer resolves at runtime.
286
+ assert.equal(result!.reviewerModels, undefined);
287
+ assert.equal(result!.model, undefined);
288
+ const summary = ui.confirmMessages.at(-1) ?? "";
289
+ assert.match(summary, /reviewer models: correctness=Pi default, security=Pi default, testing=Pi default/);
290
+ });
291
+
292
+ test("wizard cancels when the user escapes the model-assignment select", async () => {
293
+ const ui = scriptedUi({
294
+ inputs: ["review auth"],
295
+ selects: [
296
+ "code review (code)",
297
+ "Single gate (fix then re-run /rv-loop) · recommended",
298
+ "1 · host fix point (recommended)",
299
+ "Preset code-experts (correctness/security/testing)",
300
+ SELECT_CANCEL,
301
+ ],
302
+ confirms: [true],
303
+ });
304
+
305
+ const result = await runRvInteractiveWizard(ui, {
306
+ strategy: "loop",
307
+ seed: seed(),
308
+ models,
309
+ locale: "en",
310
+ });
311
+
312
+ assert.equal(result, undefined);
244
313
  });
245
314
 
246
315
  test("wizard can select until-clean with a hard budget", async () => {
@@ -257,7 +326,7 @@ test("wizard can select until-clean with a hard budget", async () => {
257
326
  "quorum · at least min-agree agree (default)",
258
327
  "2",
259
328
  ],
260
- confirms: [true, true],
329
+ confirms: [true],
261
330
  });
262
331
 
263
332
  const result = await runRvInteractiveWizard(ui, {
@@ -527,7 +596,7 @@ test("inline picker: per-reviewer models resolve through repeated picker calls",
527
596
  "1 · host fix point (recommended)",
528
597
  "Custom reviewer count 2–8 (r1..rN, pick models)",
529
598
  "3",
530
- "Pick separately for each reviewer",
599
+ "Pick a model for each reviewer",
531
600
  // thinking selects for r1, r2, r3
532
601
  "xhigh",
533
602
  "high",
@@ -535,7 +604,7 @@ test("inline picker: per-reviewer models resolve through repeated picker calls",
535
604
  "quorum · at least min-agree agree (default)",
536
605
  "2",
537
606
  ],
538
- confirms: [true, true],
607
+ confirms: [true],
539
608
  customModelPickerResults: [
540
609
  "openai/gpt-5.6-luna",
541
610
  "anthropic/claude-opus-4",
@@ -560,3 +629,105 @@ test("inline picker: per-reviewer models resolve through repeated picker calls",
560
629
  const pickerCalls = uiPanel.log.filter((e) => e.startsWith("customModelPicker:"));
561
630
  assert.equal(pickerCalls.length, 3, uiPanel.log.join("\n"));
562
631
  });
632
+
633
+ test("wizard (zh) per-reviewer branch routes on the exact zh option label", async () => {
634
+ const ui = scriptedUi({
635
+ inputs: ["@src"],
636
+ selects: [
637
+ "代码审查 (code)",
638
+ "预设 code-experts(正确性/安全/测试)",
639
+ "每位 reviewer 分别选择模型",
640
+ "openai-codex/gpt-5.6-sol",
641
+ "xhigh",
642
+ "anthropic/claude-sonnet-4-5",
643
+ "high",
644
+ "openai-codex/gpt-5.6-sol",
645
+ "xhigh",
646
+ "quorum · 至少 min-agree 人同意(默认)",
647
+ "2",
648
+ ],
649
+ confirms: [true],
650
+ });
651
+
652
+ const result = await runRvInteractiveWizard(ui, {
653
+ strategy: "panel",
654
+ seed: seed({ strategy: "panel" }),
655
+ models,
656
+ locale: "zh",
657
+ });
658
+
659
+ assert.ok(result);
660
+ assert.equal(result!.panel, "code-experts");
661
+ assert.deepEqual(result!.reviewerModels, [
662
+ "correctness=openai-codex/gpt-5.6-sol:xhigh",
663
+ "security=anthropic/claude-sonnet-4-5:high",
664
+ "testing=openai-codex/gpt-5.6-sol:xhigh",
665
+ ]);
666
+ const summary = ui.confirmMessages.at(-1) ?? "";
667
+ assert.match(summary, /reviewer 模型: correctness=openai-codex\/gpt-5\.6-sol:xhigh, security=anthropic\/claude-sonnet-4-5:high, testing=openai-codex\/gpt-5\.6-sol:xhigh/);
668
+ });
669
+
670
+ test("wizard (zh) Pi-default branch skips model prompts and shows the zh placeholder per reviewer", async () => {
671
+ const ui = scriptedUi({
672
+ inputs: ["@src"],
673
+ selects: [
674
+ "代码审查 (code)",
675
+ "预设 code-experts(正确性/安全/测试)",
676
+ "使用 Pi 默认模型(运行时解析实际模型)",
677
+ "quorum · 至少 min-agree 人同意(默认)",
678
+ "2",
679
+ ],
680
+ confirms: [true],
681
+ });
682
+
683
+ const result = await runRvInteractiveWizard(ui, {
684
+ strategy: "panel",
685
+ seed: seed({ strategy: "panel" }),
686
+ models,
687
+ locale: "zh",
688
+ });
689
+
690
+ assert.ok(result);
691
+ assert.equal(result!.panel, "code-experts");
692
+ assert.equal(result!.reviewerModels, undefined);
693
+ assert.equal(result!.model, undefined);
694
+ const summary = ui.confirmMessages.at(-1) ?? "";
695
+ assert.match(summary, /reviewer 模型: correctness=Pi 默认, security=Pi 默认, testing=Pi 默认/);
696
+ // No model picker step ran: the wizard never offered a model list select.
697
+ assert.ok(!ui.log.some((entry) => entry.startsWith("select:共用模型") || entry.startsWith("select:模型 · ")), ui.log.join("\n"));
698
+ });
699
+
700
+ test("wizard (zh) shared-model branch writes the same token for every reviewer id", async () => {
701
+ const ui = scriptedUi({
702
+ inputs: ["@src"],
703
+ selects: [
704
+ "代码审查 (code)",
705
+ "预设 code-experts(正确性/安全/测试)",
706
+ "所有 reviewer 使用同一模型",
707
+ "openai-codex/gpt-5.6-sol",
708
+ "xhigh",
709
+ "quorum · 至少 min-agree 人同意(默认)",
710
+ "2",
711
+ ],
712
+ confirms: [true],
713
+ });
714
+
715
+ const result = await runRvInteractiveWizard(ui, {
716
+ strategy: "panel",
717
+ seed: seed({ strategy: "panel" }),
718
+ models,
719
+ locale: "zh",
720
+ });
721
+
722
+ assert.ok(result);
723
+ assert.equal(result!.panel, "code-experts");
724
+ assert.deepEqual(result!.reviewerModels, [
725
+ "correctness=openai-codex/gpt-5.6-sol:xhigh",
726
+ "security=openai-codex/gpt-5.6-sol:xhigh",
727
+ "testing=openai-codex/gpt-5.6-sol:xhigh",
728
+ ]);
729
+ const summary = ui.confirmMessages.at(-1) ?? "";
730
+ assert.match(summary, /reviewer 模型: correctness=openai-codex\/gpt-5\.6-sol:xhigh, security=openai-codex\/gpt-5\.6-sol:xhigh, testing=openai-codex\/gpt-5\.6-sol:xhigh/);
731
+ // The shared-model picker (zh title) actually ran.
732
+ assert.ok(ui.log.some((entry) => entry.startsWith("select:共用模型")), ui.log.join("\n"));
733
+ });
@@ -430,77 +430,60 @@ export async function runRvInteractiveWizard(
430
430
  const labels = rankedModelLabels(models, mode);
431
431
 
432
432
  if (ids.length > 0) {
433
- const assignEach = await ui.confirm(
434
- zh ? "为每位 reviewer 选择模型?" : "Pick a model for each reviewer?",
435
- zh
436
- ? `将依次为 ${ids.join(", ")} 弹出模型列表(方向键选择,回车确认)。选「同一模型」可共用。`
437
- : `You will pick models for ${ids.join(", ")} from a list (arrows + enter). Choose "Same model for all" to share.`,
433
+ // Branch on stable keys, not display-label substrings, so copy edits in
434
+ // either language can never silently reroute the wizard.
435
+ const assignmentOptions: Array<{ key: "per-reviewer" | "shared" | "pi-default"; label: string }> = [
436
+ { key: "per-reviewer", label: zh ? "每位 reviewer 分别选择模型" : "Pick a model for each reviewer" },
437
+ { key: "shared", label: zh ? "所有 reviewer 使用同一模型" : "Same model for all reviewers" },
438
+ { key: "pi-default", label: zh ? "使用 Pi 默认模型(运行时解析实际模型)" : "Use Pi's default model (resolved at runtime)" },
439
+ ];
440
+ const assignmentChoice = await mustSelect(
441
+ ui,
442
+ zh ? "模型分配方式" : "Model assignment",
443
+ assignmentOptions.map((option) => option.label),
438
444
  );
439
-
440
- if (assignEach) {
441
- const sameFirst = await mustSelect(
442
- ui,
443
- zh ? "模型分配方式" : "Model assignment",
444
- [
445
- zh ? "每位 reviewer 分别选择" : "Pick separately for each reviewer",
446
- zh ? "所有 reviewer 使用同一模型" : "Same model for all reviewers",
447
- ],
448
- );
449
- if (sameFirst === CANCEL) return undefined;
450
-
451
- if (sameFirst.includes("Same") || sameFirst.includes("同一")) {
452
- if (labels.length === 0) {
453
- ui.notify(zh ? "当前没有可用模型目录,请先 /rv-models" : "No model catalog available; run /rv-models first", "warning");
454
- } else {
455
- const pick = await pickModelLabel(ui, models, mode, locale, zh ? "共用模型" : "Shared model", false);
456
- if (pick === CANCEL) return undefined;
457
- if (pick !== undefined) {
458
- const m = modelByLabel(models, pick);
459
- const thinkingPick = await mustSelect(
460
- ui,
461
- zh ? `思考强度(${pick})` : `Thinking (${pick})`,
462
- thinkingOptions(m, locale),
463
- );
464
- if (thinkingPick === CANCEL) return undefined;
465
- const thinking = stripSkip(thinkingPick, locale);
466
- const token = thinking ? `${pick}:${thinking}` : pick;
467
- for (const id of ids) reviewerModels.push(`${id}=${token}`);
468
- }
469
- }
445
+ if (assignmentChoice === CANCEL) return undefined;
446
+ const assignment = assignmentOptions.find((option) => option.label === assignmentChoice)?.key ?? "pi-default";
447
+
448
+ if (assignment === "per-reviewer") {
449
+ for (const id of ids) {
450
+ if (labels.length === 0) break;
451
+ const pick = await pickModelLabel(ui, models, mode, locale, zh ? `模型 · ${id}` : `Model · ${id}`, false);
452
+ if (pick === CANCEL) return undefined;
453
+ if (pick === undefined) continue; // defensive: allowSkip=false never returns undefined here
454
+ const m = modelByLabel(models, pick);
455
+ const thinkingPick = await mustSelect(
456
+ ui,
457
+ zh ? `思考强度 · ${id}` : `Thinking · ${id}`,
458
+ thinkingOptions(m, locale),
459
+ );
460
+ if (thinkingPick === CANCEL) return undefined;
461
+ const thinking = stripSkip(thinkingPick, locale);
462
+ reviewerModels.push(`${id}=${thinking ? `${pick}:${thinking}` : pick}`);
463
+ }
464
+ } else if (assignment === "shared") {
465
+ if (labels.length === 0) {
466
+ ui.notify(zh ? "当前没有可用模型目录,请先 /rv-models" : "No model catalog available; run /rv-models first", "warning");
470
467
  } else {
471
- for (const id of ids) {
472
- if (labels.length === 0) break;
473
- const pick = await pickModelLabel(ui, models, mode, locale, zh ? `模型 · ${id}` : `Model · ${id}`, false);
474
- if (pick === CANCEL) return undefined;
475
- if (pick === undefined) continue; // defensive: allowSkip=false never returns undefined here
468
+ const pick = await pickModelLabel(ui, models, mode, locale, zh ? "共用模型" : "Shared model", false);
469
+ if (pick === CANCEL) return undefined;
470
+ if (pick !== undefined) {
476
471
  const m = modelByLabel(models, pick);
477
472
  const thinkingPick = await mustSelect(
478
473
  ui,
479
- zh ? `思考强度 · ${id}` : `Thinking · ${id}`,
474
+ zh ? `思考强度(${pick})` : `Thinking (${pick})`,
480
475
  thinkingOptions(m, locale),
481
476
  );
482
477
  if (thinkingPick === CANCEL) return undefined;
483
478
  const thinking = stripSkip(thinkingPick, locale);
484
- reviewerModels.push(`${id}=${thinking ? `${pick}:${thinking}` : pick}`);
479
+ const token = thinking ? `${pick}:${thinking}` : pick;
480
+ for (const id of ids) reviewerModels.push(`${id}=${token}`);
485
481
  }
486
482
  }
487
- } else if (!seed.model && labels.length > 0) {
488
- // Shared default model when not assigning per-reviewer
489
- const pick = await pickModelLabel(ui, models, mode, locale, zh ? "默认模型(全体 reviewer)" : "Default model (all reviewers)", true);
490
- if (pick === CANCEL) return undefined;
491
- if (pick !== undefined) {
492
- const m = modelByLabel(models, pick);
493
- const thinkingPick = await mustSelect(
494
- ui,
495
- zh ? "默认思考强度" : "Default thinking",
496
- thinkingOptions(m, locale),
497
- );
498
- if (thinkingPick === CANCEL) return undefined;
499
- const thinking = stripSkip(thinkingPick, locale);
500
- seed.model = pick;
501
- if (thinking) seed.thinking = thinking;
502
- }
503
483
  }
484
+ // else: Pi default for every reviewer — no reviewerModels, no further prompt.
485
+ // The confirm summary below still shows each reviewer's resolved model
486
+ // (falling back to the shared --model flag, then a "Pi default" placeholder).
504
487
  } else if (!seed.model && labels.length > 0) {
505
488
  // Single-reviewer path
506
489
  const pick = await pickModelLabel(ui, models, mode, locale, zh ? "模型" : "Model", true);
@@ -543,6 +526,23 @@ export async function runRvInteractiveWizard(
543
526
  }
544
527
 
545
528
  // 7) Confirm summary
529
+ // Per-reviewer effective model for the confirm summary: explicit override →
530
+ // shared --model (if set) → "Pi default" placeholder. Mirrors what the CLI
531
+ // actually resolves at runtime (resolveReviewerModelThinking).
532
+ const piDefaultLabel = zh ? "Pi 默认" : "Pi default";
533
+ const reviewerModelOverrides = new Map(
534
+ reviewerModels.map((entry) => {
535
+ const eq = entry.indexOf("=");
536
+ return [entry.slice(0, eq), entry.slice(eq + 1)] as const;
537
+ }),
538
+ );
539
+ const reviewerModelSummary = ids.map((id) => {
540
+ const override = reviewerModelOverrides.get(id);
541
+ if (override) return `${id}=${override}`;
542
+ if (seed.model) return `${id}=${seed.model}${seed.thinking ? `:${seed.thinking}` : ""}`;
543
+ return `${id}=${piDefaultLabel}`;
544
+ });
545
+
546
546
  const summaryLines = [
547
547
  `${zh ? "策略" : "Strategy"}: ${strategy}`,
548
548
  `${zh ? "目标" : "Target"}: ${target}`,
@@ -559,8 +559,11 @@ export async function runRvInteractiveWizard(
559
559
  : zh
560
560
  ? "单 reviewer"
561
561
  : "single reviewer",
562
- seed.model ? `model: ${seed.model}${seed.thinking ? `:${seed.thinking}` : ""}` : "",
563
- reviewerModels.length ? `reviewer-models: ${reviewerModels.join(", ")}` : "",
562
+ // Multi-reviewer paths show one resolved model per reviewer id; single-reviewer
563
+ // and shared-model paths keep the existing single `model:` line.
564
+ ids.length > 0
565
+ ? `${zh ? "reviewer 模型" : "reviewer models"}: ${reviewerModelSummary.join(", ")}`
566
+ : `model: ${seed.model ? `${seed.model}${seed.thinking ? `:${seed.thinking}` : ""}` : piDefaultLabel}`,
564
567
  consensus ? `consensus: ${consensus}${minAgree ? ` min-agree=${minAgree}` : ""}` : "",
565
568
  ].filter(Boolean);
566
569
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zephyrdeng/pi-review",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Isolated AI-powered code and plan reviews from the command line",
5
5
  "type": "module",
6
6
  "author": "ZephyrDeng <zephyrTang@aliyun.com>",
@@ -39,12 +39,14 @@ Prioritized findings. For every material finding, use this exact shape (incremen
39
39
  ### F1: <summary>
40
40
  - Severity: critical | high | medium | low
41
41
  - Path: <path or none>
42
+ - Lines: <line or line-range in Path, or none>
43
+ - Side: base | working (optional; defaults to working)
42
44
  - Actionable: yes | no
43
45
  - Evidence: <concrete evidence>
44
46
  - Impact: <why it matters>
45
47
  - Recommendation: <specific next step>
46
48
 
47
- Set Actionable to yes when the host must fix or consciously reject the finding before clean closeout. Use no for informational or explicitly out-of-scope follow-up findings. If there are no material findings, write "No material findings."
49
+ Set Actionable to yes when the host must fix or consciously reject the finding before clean closeout. Use no for informational or explicitly out-of-scope follow-up findings. Include Lines only when you have reliable line numbers for Path: a single line (`42`) or an inclusive range (`42-58`). Omit Lines or write "none" for file-level or non-line-specific findings — never guess a line number. Side marks which half of a diff Lines refers to (`base` = before the change, `working` = after); omit it to default to `working`. If there are no material findings, write "No material findings."
48
50
 
49
51
  ## Risks and Blind Spots
50
52
  What could still be wrong, missing, or unverified.
@@ -27,12 +27,12 @@ node ../../bin/pi-review.js --help
27
27
  |------|-------------------------|
28
28
  | **Pi** (`/rv`) | New panel reviews call the native `pi_review` API tool, shown to users as **Pi Review Panel**, which launches the packaged CLI in event mode and renders live reviewer state. Continuations, kept sessions, `/rv-loop`, and explicit `--no-stream` use the shell CLI. |
29
29
  | **Pi terminal** | Foreground `pi-review`; text deltas stream live by default. |
30
- | **Claude Code / Codex / Cursor** and similar agent hosts | These hosts buffer a shell tool's output until the command exits, so a foreground run looks like a silent wait. **Panel review (`--reviewers`/`--panel`) default: `--ui web --ui-url-file <path>` + background run** — hand the user the printed dashboard URL so they watch live progress in a browser instead of the chat. **Single review / `loop` default: `--progress-log <path>` + background run + tail the log** (`--ui web` requires an active panel and rejects `loop`). See [references/codex-tools.md](./references/codex-tools.md). Map skill mentions of `Bash` to your host shell tool. |
30
+ | **Claude Code / Codex / Cursor** and similar agent hosts | These hosts buffer a shell tool's output until the command exits, so a foreground run looks like a silent wait. **Panel review (`--reviewers`/`--panel`) default: `--ui web --ui-url-file <path>` + background run** — get the dashboard in front of the user: on Claude Code the CLI auto-open lands in the system default browser; on Codex open the `--ui-url-file` URL in the host's built-in browser (the CLI cannot reach a GUI there); hand over the URL as fallback either way. **Single review / `loop` default: `--progress-log <path>` + background run + tail the log** (`--ui web` requires an active panel and rejects `loop`). See [references/codex-tools.md](./references/codex-tools.md). Map skill mentions of `Bash` to your host shell tool. |
31
31
  | Scripts / CI | Foreground is fine; add `--no-stream` only when the caller must buffer until exit (cannot combine with `--progress-log`). |
32
32
 
33
33
  `--progress-log <path>` tees the raw `--mode json` event stream to a file for outside observation and debugging; it is never a prerequisite for metrics. Token usage accumulates by default, and `pi-review` writes milestone notices (`pi-review: review started`, `pi-review: tool <name> started/finished`, `pi-review: review finished`) to stderr.
34
34
 
35
- `--ui web` starts a loopback-only browser dashboard for panel review and auto-opens it in the default browser (`--no-ui-open` disables the auto-open); `--ui-url-file <path>` writes its URL atomically so a buffered-output host can read it without parsing stderr. After completion the page counts down 60s, then closes itself and stops the dashboard server (any interaction cancels the countdown). It never substitutes for panel semantics — findings, gate status, and exit code come from the same `PI_REVIEW_META_JSON` as always.
35
+ `--ui web` starts a loopback-only browser dashboard for panel review and auto-opens it in the default browser (`--no-ui-open` disables the auto-open); `--ui-url-file <path>` writes its URL atomically so a buffered-output host can read it without parsing stderr. Open the dashboard as wide as the host allows: Claude Code rides the auto-open (system default browser), Codex opens the url-file URL in its built-in browser, and Pi renders panels natively so `/rv` needs no `--ui web`. After completion the page counts down 60s, then closes itself and stops the dashboard server (any interaction cancels the countdown). It never substitutes for panel semantics — findings, gate status, and exit code come from the same `PI_REVIEW_META_JSON` as always.
36
36
 
37
37
  ## Run metrics
38
38
 
@@ -40,7 +40,7 @@ Quote metrics from the run output; never estimate or invent them.
40
40
 
41
41
  - **ASCII footer** (the `── pi-review` / `── pi-review panel` block): Duration, Tokens as `in · out · cache · reason` plus total, and Cost — the provider-reported value, or `n/a` when the provider reports none. On Pi `/rv` the panel tool result is **plain text / ASCII only** (Pi does not render markdown in tool results) — same classic chrome as the CLI, no `###` headings or fenced code blocks.
42
42
  - **Pi Review Panel**: each reviewer row shows **role persona · lifecycle · model · thinking · elapsed · tokens**. Lifecycle is `queued` / `running` / `completed` / `failed` / `cancelled`. Expanded view adds bounded activity, plain-text findings, and the ASCII footer.
43
- - Machine output: parse `PI_REVIEW_META_JSON:` from **stderr** (or set `PI_REVIEW_META_STDOUT=1` to emit it on stdout). Show users the plain review text and ASCII footer, not raw JSON, unless they ask for machine output. Do **not** rewrap the tool result into markdown tables or `###` report sections.
43
+ - Machine output: parse `PI_REVIEW_META_JSON:` from **stderr** (or set `PI_REVIEW_META_STDOUT=1` to emit it on stdout). Show users the plain review text and ASCII footer, not raw JSON, unless they ask for machine output. Do **not** rewrap the tool result into markdown tables or `###` report sections. On panel runs the top-level `model` is the reviewers' shared effective model (configured, else provider-reported) or the literal sentinel `"mixed"` when reviewers ran on different models; per-reviewer entries carry their own `model`/`responseModel`.
44
44
 
45
45
  ## Pi host (`/rv*`)
46
46
 
@@ -185,11 +185,12 @@ pi-review --reviewers 3 --ui web --ui-url-file /tmp/pi-review-ui-url.txt -- @src
185
185
  ```
186
186
  Rules:
187
187
  - Run this before building the review command. **Never invent a `provider/model` id** — only ids from the catalog output.
188
- - Follow **[references/model-selection.md](./references/model-selection.md)** for everything else: profile inference (code / frontend / plan), priority lists, user-named-model resolution, and thinking aliases (`max`/`最高` → `xhigh`, with fallback to the nearest supported level).
188
+ - **Cross-model review**: identify the model this host session itself runs on (its id is in your session context), then prefer reviewer models from a different model family — same-family reviewers share the author model's blind spots. On a panel, keep the host's family to a minority of reviewers.
189
+ - Follow **[references/model-selection.md](./references/model-selection.md)** for everything else: profile inference (code / frontend / plan), priority lists, cross-model resolution, user-named-model resolution, and thinking aliases (`max`/`最高` → `xhigh`, with fallback to the nearest supported level).
189
190
  - Ambiguous match across model families: show the top candidates and ask; do not guess.
190
- - No priority match: omit `--model` (Pi default) or pick a listed reasoning model with a large context window — say which and why.
191
+ - User named no model: propose 2–3 exact catalog ids (cross-model first, one line of why each, "Pi default" as an explicit option) and ask the user to pick before running.
191
192
  - On **Claude Code / Codex / Cursor**, state the chosen model in one line (match the user's language) before running the review command.
192
- Completion criterion: exact listed `provider/model[:thinking]` or an explicit default; never invented ids.
193
+ Completion criterion: an exact listed `provider/model[:thinking]` (or Pi default) that the user named or picked from your proposal; never invented ids.
193
194
 
194
195
  2. Choose one mode:
195
196
  - default `code` — code, diff, MR, file, or repository review.
@@ -8,7 +8,7 @@ Skills may use Claude Code tool names. On **Codex** or **Claude Code**, map them
8
8
  | `Read`, `Write`, `Edit` | Native file tools |
9
9
  | `Skill` | Load and follow the skill instructions directly |
10
10
 
11
- General Codex subagent and worktree mappings live in the **using-superpowers** skill (`references/codex-tools.md`) when that skill is installed; this file is **pi-review–specific** and ships with the pi-review package.
11
+ This file is **pi-review–specific** and ships with the pi-review package.
12
12
 
13
13
  ## Long-running `pi-review` (default on Codex / Claude Code)
14
14
 
@@ -18,8 +18,8 @@ Native shell tools on these hosts **buffer stdout/stderr until the command exits
18
18
 
19
19
  `--reviewers <n>` or `--panel <name>` activates panel review. On Codex, Claude Code, or Cursor, default to the dashboard instead of a tailed log:
20
20
 
21
- 1. Run `pi-review --reviewers <n> --ui web --ui-url-file <path> -- <target>` (or `--panel <name>`) in the **background**. The dashboard auto-opens in the user's default browser once the server is ready; pass `--no-ui-open` only when the user asked not to open a browser (CI, remote shells).
22
- 2. Read `<path>` shortly after launch — it is written atomically as soon as the dashboard server is ready, well before reviewers finish (poll a few times a second for a couple of seconds if the file is not there yet). Share that URL with the user as a fallback: *"Dashboard opened in your browser (URL: `<url>`). I'll report back when the review completes."*
21
+ 1. Run `pi-review --reviewers <n> --ui web --ui-url-file <path> -- <target>` (or `--panel <name>`) in the **background**. Pass `--no-ui-open` only when the user asked not to open a browser (CI, remote shells).
22
+ 2. Read `<path>` shortly after launch — it is written atomically as soon as the dashboard server is ready, well before reviewers finish (poll a few times a second for a couple of seconds if the file is not there yet). Then get the dashboard in front of the user: on **Claude Code** the CLI auto-open already lands in the system default browser; on **Codex** the CLI cannot reach a GUI browser, so open the URL from `<path>` in the host's built-in browser yourself. Share the URL either way as a fallback: *"Dashboard opened in your browser (URL: `<url>`). I'll report back when the review completes."*
23
23
  3. Wait for the background job. On exit, show the Markdown review body and ASCII `── pi-review` footer as usual (skill step 4) — the dashboard is a presentation layer only; findings, gate status, and exit code always come from `PI_REVIEW_META_JSON`.
24
24
  4. `--ui web` requires an active panel and rejects `loop` — for a single review or a panel `loop`, fall back to the progress-log workflow below.
25
25
 
@@ -6,11 +6,11 @@ Use this after `pi-review models` (or `pi-review models <search>`). **Never inve
6
6
 
7
7
  1. Run `pi-review models` (narrow with search if the user named a vendor or model family).
8
8
  2. Infer **review profile** from the target (see table below).
9
- 3. Walk the **priority list** for that profile; pick the **first** row whose `idContains` matches some listed model id (case-insensitive substring). If several ids match one row, prefer the **newest** id (higher version suffix, e.g. `2.7` over `2.5`, `4-8` over `4-6`).
9
+ 3. **Cross-model review**: identify the host session's own model — the model that wrote or will fix the code under review; its id is in your own session context. Walk the **priority list** for that profile skipping rows in the host's model family, and pick the **first** remaining row whose `idContains` matches some listed model id (case-insensitive substring). Same-family reviewers share the author model's blind spots; return to the host's own family only when no other family matches the catalog. On a panel, spread reviewer models so the host's family stays a minority. If several ids match one row, prefer the **newest** id (higher version suffix, e.g. `2.7` over `2.5`, `4-8` over `4-6`).
10
10
  4. Set `--model <exact-listed-provider/model>` from the catalog. Add `:thinking` only if that model supports it in Pi and the preset suggests it (see thinking column).
11
11
  - User “max / 最高档” maps to Pi thinking `xhigh` when supported; otherwise fall back to the highest available level.
12
12
  - User “low / off / medium / high” maps to that exact level when supported — **do not upgrade** a user-specified low/off to high/xhigh.
13
- 5. If **no** priority row matches anything in the catalog, either omit `--model` (Pi default) or pick the best **reasoning** model with a large context window from the list—state the choice in one line to the user.
13
+ 5. When the user named no model, the walk above yields a **proposal, not a decision**: present 2–3 exact catalog ids (cross-model candidates first, one line of why each, plus “Pi default” as an explicit option) and ask the user to pick before running the review. A user-named model skips the ask and resolves per **User-named model** below. If **no** priority row matches anything in the catalog, propose the best **reasoning** models with a large context window from the list the same way.
14
14
 
15
15
  ## Profiles
16
16