@zephyrdeng/pi-review 0.7.1 → 0.8.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.
Files changed (181) hide show
  1. package/README.md +37 -8
  2. package/README.zh-CN.md +31 -5
  3. package/dist/args.d.ts +4 -0
  4. package/dist/args.d.ts.map +1 -1
  5. package/dist/args.js +61 -4
  6. package/dist/args.js.map +1 -1
  7. package/dist/args.test.js +62 -1
  8. package/dist/args.test.js.map +1 -1
  9. package/dist/child-process.d.ts +3 -0
  10. package/dist/child-process.d.ts.map +1 -1
  11. package/dist/child-process.js +27 -5
  12. package/dist/child-process.js.map +1 -1
  13. package/dist/child-process.test.js +42 -1
  14. package/dist/child-process.test.js.map +1 -1
  15. package/dist/cli-exit.test.js +14 -5
  16. package/dist/cli-exit.test.js.map +1 -1
  17. package/dist/cli.js +8 -3
  18. package/dist/cli.js.map +1 -1
  19. package/dist/index.d.ts +7 -5
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +5 -4
  22. package/dist/index.js.map +1 -1
  23. package/dist/install-args.test.js +1 -1
  24. package/dist/install-args.test.js.map +1 -1
  25. package/dist/install.d.ts.map +1 -1
  26. package/dist/install.js +2 -3
  27. package/dist/install.js.map +1 -1
  28. package/dist/json-events.d.ts +6 -0
  29. package/dist/json-events.d.ts.map +1 -1
  30. package/dist/json-events.js +48 -6
  31. package/dist/json-events.js.map +1 -1
  32. package/dist/json-events.test.js +20 -1
  33. package/dist/json-events.test.js.map +1 -1
  34. package/dist/loop.d.ts +28 -2
  35. package/dist/loop.d.ts.map +1 -1
  36. package/dist/loop.js +45 -3
  37. package/dist/loop.js.map +1 -1
  38. package/dist/loop.test.js +24 -1
  39. package/dist/loop.test.js.map +1 -1
  40. package/dist/matcher.test.js +1 -1
  41. package/dist/matcher.test.js.map +1 -1
  42. package/dist/meta-footer.d.ts +4 -8
  43. package/dist/meta-footer.d.ts.map +1 -1
  44. package/dist/meta-footer.js +19 -5
  45. package/dist/meta-footer.js.map +1 -1
  46. package/dist/meta-footer.test.js +11 -4
  47. package/dist/meta-footer.test.js.map +1 -1
  48. package/dist/panel-aggregate.test.js +1 -1
  49. package/dist/panel-aggregate.test.js.map +1 -1
  50. package/dist/panel-cancellation.test.js +33 -2
  51. package/dist/panel-cancellation.test.js.map +1 -1
  52. package/dist/panel-cli-ui.test.d.ts +2 -0
  53. package/dist/panel-cli-ui.test.d.ts.map +1 -0
  54. package/dist/panel-cli-ui.test.js +91 -0
  55. package/dist/panel-cli-ui.test.js.map +1 -0
  56. package/dist/panel-cli.test.js +37 -39
  57. package/dist/panel-cli.test.js.map +1 -1
  58. package/dist/panel-config.d.ts +38 -0
  59. package/dist/panel-config.d.ts.map +1 -1
  60. package/dist/panel-config.js +135 -2
  61. package/dist/panel-config.js.map +1 -1
  62. package/dist/panel-config.test.js +79 -2
  63. package/dist/panel-config.test.js.map +1 -1
  64. package/dist/panel-loop.test.js +1 -1
  65. package/dist/panel-loop.test.js.map +1 -1
  66. package/dist/panel-tools.test.js +1 -1
  67. package/dist/panel-tools.test.js.map +1 -1
  68. package/dist/panel-ui.d.ts +20 -0
  69. package/dist/panel-ui.d.ts.map +1 -0
  70. package/dist/panel-ui.js +84 -0
  71. package/dist/panel-ui.js.map +1 -0
  72. package/dist/panel-ui.test.d.ts +2 -0
  73. package/dist/panel-ui.test.d.ts.map +1 -0
  74. package/dist/panel-ui.test.js +122 -0
  75. package/dist/panel-ui.test.js.map +1 -0
  76. package/dist/panel-view.d.ts +1 -0
  77. package/dist/panel-view.d.ts.map +1 -1
  78. package/dist/panel-view.js +40 -6
  79. package/dist/panel-view.js.map +1 -1
  80. package/dist/panel-view.test.js +68 -1
  81. package/dist/panel-view.test.js.map +1 -1
  82. package/dist/panel.d.ts +11 -1
  83. package/dist/panel.d.ts.map +1 -1
  84. package/dist/panel.js +67 -21
  85. package/dist/panel.js.map +1 -1
  86. package/dist/prompt.d.ts +2 -0
  87. package/dist/prompt.d.ts.map +1 -1
  88. package/dist/prompt.js +34 -3
  89. package/dist/prompt.js.map +1 -1
  90. package/dist/prompt.test.js +21 -2
  91. package/dist/prompt.test.js.map +1 -1
  92. package/dist/review-events.test.js +1 -1
  93. package/dist/review-events.test.js.map +1 -1
  94. package/dist/review-result.test.js +1 -1
  95. package/dist/review-result.test.js.map +1 -1
  96. package/dist/review.js +3 -3
  97. package/dist/review.js.map +1 -1
  98. package/dist/review.meta.test.js +1 -1
  99. package/dist/review.meta.test.js.map +1 -1
  100. package/dist/skill-protocol.test.js +1 -1
  101. package/dist/skill-protocol.test.js.map +1 -1
  102. package/dist/skill-update.test.d.ts +2 -0
  103. package/dist/skill-update.test.d.ts.map +1 -0
  104. package/dist/skill-update.test.js +23 -0
  105. package/dist/skill-update.test.js.map +1 -0
  106. package/dist/skill.d.ts +22 -0
  107. package/dist/skill.d.ts.map +1 -1
  108. package/dist/skill.js +109 -22
  109. package/dist/skill.js.map +1 -1
  110. package/dist/types.d.ts +23 -1
  111. package/dist/types.d.ts.map +1 -1
  112. package/dist/types.js.map +1 -1
  113. package/dist/ui-client.d.ts +13 -0
  114. package/dist/ui-client.d.ts.map +1 -0
  115. package/dist/ui-client.js +161 -0
  116. package/dist/ui-client.js.map +1 -0
  117. package/dist/ui-client.test.d.ts +2 -0
  118. package/dist/ui-client.test.d.ts.map +1 -0
  119. package/dist/ui-client.test.js +48 -0
  120. package/dist/ui-client.test.js.map +1 -0
  121. package/dist/ui-html.d.ts +7 -0
  122. package/dist/ui-html.d.ts.map +1 -0
  123. package/dist/ui-html.js +52 -0
  124. package/dist/ui-html.js.map +1 -0
  125. package/dist/ui-html.test.d.ts +2 -0
  126. package/dist/ui-html.test.d.ts.map +1 -0
  127. package/dist/ui-html.test.js +26 -0
  128. package/dist/ui-html.test.js.map +1 -0
  129. package/dist/ui-server-main.d.ts +3 -0
  130. package/dist/ui-server-main.d.ts.map +1 -0
  131. package/dist/ui-server-main.js +128 -0
  132. package/dist/ui-server-main.js.map +1 -0
  133. package/dist/ui-server-main.test.d.ts +2 -0
  134. package/dist/ui-server-main.test.d.ts.map +1 -0
  135. package/dist/ui-server-main.test.js +123 -0
  136. package/dist/ui-server-main.test.js.map +1 -0
  137. package/dist/ui-server.d.ts +42 -0
  138. package/dist/ui-server.d.ts.map +1 -0
  139. package/dist/ui-server.js +233 -0
  140. package/dist/ui-server.js.map +1 -0
  141. package/dist/ui-server.test.d.ts +2 -0
  142. package/dist/ui-server.test.d.ts.map +1 -0
  143. package/dist/ui-server.test.js +236 -0
  144. package/dist/ui-server.test.js.map +1 -0
  145. package/dist/update.d.ts +2 -0
  146. package/dist/update.d.ts.map +1 -1
  147. package/dist/update.js +80 -20
  148. package/dist/update.js.map +1 -1
  149. package/dist/update.test.d.ts +2 -0
  150. package/dist/update.test.d.ts.map +1 -0
  151. package/dist/update.test.js +17 -0
  152. package/dist/update.test.js.map +1 -0
  153. package/extensions/panel-tool.test.ts +366 -8
  154. package/extensions/panel-tool.ts +289 -68
  155. package/extensions/review.test.ts +79 -0
  156. package/extensions/review.ts +188 -44
  157. package/extensions/rv-completions.test.ts +177 -18
  158. package/extensions/rv-completions.ts +490 -60
  159. package/extensions/rv-interactive.test.ts +562 -0
  160. package/extensions/rv-interactive.ts +616 -0
  161. package/extensions/rv-locale.test.ts +12 -4
  162. package/extensions/rv-locale.ts +44 -11
  163. package/extensions/rv-model-picker.test.ts +172 -0
  164. package/extensions/rv-model-picker.ts +313 -0
  165. package/extensions/rv-model-priorities.test.ts +29 -9
  166. package/extensions/rv-model-priorities.ts +23 -8
  167. package/extensions/rv-prompts.test.ts +274 -2
  168. package/extensions/rv-prompts.ts +479 -33
  169. package/extensions/rv-resolve.test.ts +189 -0
  170. package/extensions/rv-resolve.ts +313 -0
  171. package/extensions/rv-semantic.test.ts +1 -1
  172. package/extensions/rv-semantic.ts +38 -4
  173. package/package.json +7 -4
  174. package/resources/rv-model-priorities.json +18 -9
  175. package/skills/pi-review/SKILL.md +84 -60
  176. package/skills/pi-review/references/codex-tools.md +18 -11
  177. package/skills/pi-review/references/model-selection.md +49 -28
  178. package/dist/extensions/review.d.ts +0 -2
  179. package/dist/extensions/review.js +0 -22
  180. package/dist/extensions/rv-prompts.d.ts +0 -19
  181. package/dist/extensions/rv-prompts.js +0 -132
@@ -1,13 +1,45 @@
1
1
  import assert from "node:assert/strict";
2
- import { test } from "node:test";
2
+ import { test } from "vitest";
3
3
  import { createPanelViewState, reducePanelEvent } from "@zephyrdeng/pi-review";
4
- import { renderPanelResult } from "./panel-tool.js";
4
+ import {
5
+ buildPanelResultContent,
6
+ expandHint,
7
+ panelToolParamError,
8
+ PanelResultView,
9
+ registerPanelReviewTool,
10
+ renderPanelResult,
11
+ } from "./panel-tool.js";
5
12
 
6
13
  const theme = {
7
14
  fg: (_color: "toolTitle" | "muted" | "error" | "success" | "accent", text: string) => text,
8
15
  bold: (text: string) => text,
9
16
  };
10
17
 
18
+ test("panel tool boundary rejects single-reviewer and out-of-range widths", () => {
19
+ assert.match(panelToolParamError({ target: "@src", reviewers: 1 }) ?? "", /between 2 and 8/);
20
+ assert.match(panelToolParamError({ target: "@src", reviewers: 9 }) ?? "", /between 2 and 8/);
21
+ assert.match(panelToolParamError({ target: "@src", panel: "code-experts", reviewers: 2 }) ?? "", /cannot be combined/);
22
+ assert.equal(panelToolParamError({ target: "@src", reviewers: 2 }), undefined);
23
+ assert.equal(panelToolParamError({ target: "@src", panel: "code-experts" }), undefined);
24
+ });
25
+
26
+ test("registered pi_review rejects reviewers=1 before spawning the panel CLI", async () => {
27
+ let registered: { execute: (...args: any[]) => Promise<any> } | undefined;
28
+ registerPanelReviewTool({
29
+ registerTool(tool: { execute: (...args: any[]) => Promise<any> }) {
30
+ registered = tool;
31
+ },
32
+ } as never);
33
+ assert.ok(registered);
34
+ const result = await registered!.execute(
35
+ "tool-call",
36
+ { target: "@src", reviewers: 1 },
37
+ new AbortController().signal,
38
+ );
39
+ assert.equal(result.isError, true);
40
+ assert.match(result.content[0].text, /between 2 and 8/);
41
+ });
42
+
11
43
  test("Pi panel renderer exposes compact reviewer progress and expanded activity", () => {
12
44
  const now = Date.now();
13
45
  const started = reducePanelEvent(createPanelViewState(), {
@@ -18,13 +50,339 @@ test("Pi panel renderer exposes compact reviewer progress and expanded activity"
18
50
  type: "panel.started",
19
51
  target: "@src",
20
52
  mode: "code",
21
- reviewers: [{ reviewerId: "security", role: "Security", model: "openai/gpt" }],
53
+ reviewers: [{ reviewerId: "security", role: "Security", model: "openai/gpt", thinking: "high" }],
22
54
  });
23
55
  const reviewerStarted = reducePanelEvent(started, { v: 1, runId: "run-1", seq: 2, at: now, type: "reviewer.started", reviewerId: "security" });
24
- const running = reducePanelEvent(reviewerStarted, { v: 1, runId: "run-1", seq: 3, at: now, type: "reviewer.tool.started", reviewerId: "security", tool: "read" });
25
- const compact = renderPanelResult({ state: running }, false, theme).render(120).join("\n");
26
- const expanded = renderPanelResult({ state: running }, true, theme).render(120).join("\n");
27
- assert.match(compact, /pi-review panel 0\/1 completed/);
28
- assert.match(compact, /security Security · read · 0s/);
56
+ const running = reducePanelEvent(reviewerStarted, { v: 1, runId: "run-1", seq: 3, at: now - 4500, type: "reviewer.tool.started", reviewerId: "security", tool: "read", summary: "src/panel.ts" });
57
+ const compact = renderPanelResult({ state: running }, false, theme, undefined, false, now).render(120).join("\n");
58
+ const expanded = renderPanelResult({ state: running }, true, theme, undefined, false, now).render(120).join("\n");
59
+ assert.match(compact, /Pi Review Panel 0\/1 completed/);
60
+ assert.match(compact, /security Security · running · openai\/gpt · high · read · src\/panel\.ts · 0s · idle 4s/);
61
+ assert.match(compact, /expand/i);
29
62
  assert.match(expanded, /security activity/);
63
+ assert.doesNotMatch(expanded, /to expand/);
64
+ });
65
+
66
+ test("Pi panel renderer reuses the same PanelResultView across updates", () => {
67
+ const now = Date.now();
68
+ let state = reducePanelEvent(createPanelViewState(), {
69
+ v: 1,
70
+ runId: "run-reuse",
71
+ seq: 1,
72
+ at: now,
73
+ type: "panel.started",
74
+ target: "@src",
75
+ mode: "code",
76
+ reviewers: [{ reviewerId: "r1", role: "Independent reviewer", model: "openai/gpt", thinking: "high" }],
77
+ });
78
+ const first = renderPanelResult({ state }, false, theme, undefined, true, now);
79
+ assert.ok(first instanceof PanelResultView);
80
+
81
+ state = reducePanelEvent(state, { v: 1, runId: "run-reuse", seq: 2, at: now, type: "reviewer.started", reviewerId: "r1" });
82
+ const second = renderPanelResult({ state }, false, theme, first, true, now);
83
+ assert.equal(second, first);
84
+
85
+ const expanded = renderPanelResult({ state }, true, theme, second, true, now);
86
+ assert.equal(expanded, first);
87
+ assert.match(expanded.render(120).join("\n"), /r1 Independent reviewer · running/);
88
+ });
89
+
90
+ test("expandHint stays usable outside a live Pi session", () => {
91
+ assert.match(expandHint(), /expand/i);
92
+ });
93
+
94
+ test("Pi panel partial renderer shows explicit lifecycle state and uses the human-facing name", () => {
95
+ const now = Date.now();
96
+ let state = reducePanelEvent(createPanelViewState(), {
97
+ v: 1,
98
+ runId: "run-live",
99
+ seq: 1,
100
+ at: now,
101
+ type: "panel.started",
102
+ target: "@src",
103
+ mode: "code",
104
+ reviewers: [
105
+ { reviewerId: "r1", role: "Independent reviewer", model: "openai/gpt", thinking: "high" },
106
+ { reviewerId: "r2", role: "Independent reviewer", model: "openai/gpt", thinking: "high" },
107
+ ],
108
+ });
109
+ state = reducePanelEvent(state, { v: 1, runId: "run-live", seq: 2, at: now, type: "reviewer.started", reviewerId: "r1" });
110
+ state = reducePanelEvent(state, { v: 1, runId: "run-live", seq: 3, at: now, type: "reviewer.tool.started", reviewerId: "r1", tool: "read", summary: "src/panel.ts" });
111
+
112
+ const partial = renderPanelResult({ state }, false, theme, undefined, true).render(160).join("\n");
113
+ assert.match(partial, /Pi Review Panel/);
114
+ assert.doesNotMatch(partial, /pi_review/);
115
+ assert.match(partial, /r1 Independent reviewer · running · openai\/gpt · high · read · src\/panel\.ts/);
116
+ assert.match(partial, /r2 Independent reviewer · queued · openai\/gpt · high/);
117
+ });
118
+
119
+ test("Pi panel tool call uses the same human-facing name as its result", () => {
120
+ let registered: { renderCall: (...args: any[]) => any } | undefined;
121
+ registerPanelReviewTool({
122
+ registerTool(tool: { renderCall: (...args: any[]) => any }) {
123
+ registered = tool;
124
+ },
125
+ } as never);
126
+ assert.ok(registered);
127
+ const rendered = registered!.renderCall(
128
+ { target: "@src", reviewers: 2, mode: "code" },
129
+ theme,
130
+ { lastComponent: undefined },
131
+ ).render(160).join("\n");
132
+ assert.match(rendered, /Pi Review Panel/);
133
+ assert.doesNotMatch(rendered, /pi_review/);
134
+ });
135
+
136
+ test("Pi panel renderer expands final findings and reviewer provenance", () => {
137
+ const now = Date.now();
138
+ let state = reducePanelEvent(createPanelViewState(), {
139
+ v: 1,
140
+ runId: "run-1",
141
+ seq: 1,
142
+ at: now,
143
+ type: "panel.started",
144
+ target: "@src",
145
+ mode: "code",
146
+ reviewers: [{ reviewerId: "security", role: "Security", model: "openai/gpt", thinking: "high" }],
147
+ });
148
+ state = reducePanelEvent(state, {
149
+ v: 1,
150
+ runId: "run-1",
151
+ seq: 2,
152
+ at: now,
153
+ type: "panel.completed",
154
+ meta: {
155
+ strategy: "panel",
156
+ reviewMode: "code",
157
+ status: "has_findings",
158
+ verdict: "request_changes",
159
+ verdictSource: "parsed",
160
+ findings: [],
161
+ actionableCount: 1,
162
+ durationMs: 1200,
163
+ model: "openai/gpt",
164
+ configuredReviewers: 1,
165
+ successfulReviewers: 1,
166
+ consensusPolicy: "quorum",
167
+ consensusThreshold: 1,
168
+ panelHealth: "healthy",
169
+ confirmedClusters: [{ id: "C1", summary: "Auth bypass", supportingReviewerIds: ["security"], sourceFindingIds: ["F1"], confirmed: true, severity: "high", path: "src/auth.ts", supportCount: 1, actionableSupportCount: 1 }],
170
+ advisories: [{ id: "C2", summary: "Naming nit", supportingReviewerIds: ["security"], sourceFindingIds: ["F2"], confirmed: false, supportCount: 1, actionableSupportCount: 0 }],
171
+ reviewers: [{ reviewerId: "security", role: "Security", model: "openai/gpt", thinking: "high", durationMs: 1100, status: "has_findings", verdict: "request_changes", verdictSource: "parsed", contributed: true }],
172
+ adjudicationUsed: false,
173
+ },
174
+ });
175
+ const expanded = renderPanelResult({ state }, true, theme).render(120).join("\n");
176
+ assert.match(expanded, /── pi-review panel/);
177
+ assert.match(expanded, /Confirmed findings:|Auth bypass/);
178
+ assert.match(expanded, /Auth bypass/);
179
+ assert.match(expanded, /Advisories \(non-blocking\)|Naming nit/);
180
+ assert.match(expanded, /Naming nit/);
181
+ assert.match(expanded, /security/);
182
+ assert.match(expanded, /openai\/gpt/);
183
+ assert.match(expanded, /HAS FINDINGS|has_findings/);
184
+ // No markdown chrome in the expanded tool view.
185
+ assert.doesNotMatch(expanded, /### |```/);
186
+
187
+ state = reducePanelEvent(createPanelViewState(), {
188
+ v: 1,
189
+ runId: "run-2",
190
+ seq: 1,
191
+ at: now,
192
+ type: "panel.completed",
193
+ meta: {
194
+ strategy: "panel",
195
+ reviewMode: "code",
196
+ status: "clean",
197
+ verdict: "approve",
198
+ verdictSource: "parsed",
199
+ findings: [],
200
+ actionableCount: 0,
201
+ durationMs: 10,
202
+ model: null,
203
+ configuredReviewers: 0,
204
+ successfulReviewers: 0,
205
+ consensusPolicy: "quorum",
206
+ consensusThreshold: 1,
207
+ panelHealth: "healthy",
208
+ confirmedClusters: [],
209
+ advisories: [],
210
+ reviewers: [],
211
+ adjudicationUsed: false,
212
+ },
213
+ });
214
+ const empty = renderPanelResult({ state }, true, theme).render(120).join("\n");
215
+ // Clean panel with no findings: ASCII footer only (no empty ### sections).
216
+ assert.match(empty, /── pi-review panel/);
217
+ assert.match(empty, /Status\s+CLEAN|CLEAN/);
218
+ assert.match(empty, /Confirmed\s+0 actionable/);
219
+ assert.match(empty, /Advisories\s+0 non-blocking/);
220
+ });
221
+
222
+ test("Pi panel result content includes duration, token, and cost metrics", () => {
223
+ const state = reducePanelEvent(createPanelViewState(), {
224
+ v: 1,
225
+ runId: "run-metrics",
226
+ seq: 1,
227
+ at: Date.now(),
228
+ type: "panel.completed",
229
+ meta: {
230
+ strategy: "panel",
231
+ reviewMode: "code",
232
+ status: "clean",
233
+ verdict: "approve",
234
+ verdictSource: "parsed",
235
+ findings: [],
236
+ actionableCount: 0,
237
+ durationMs: 1234,
238
+ model: "openai/gpt",
239
+ usage: { input: 900, output: 200, cacheRead: 100, cacheWrite: 0, reasoning: 50, totalTokens: 1200, costTotal: 0.1234 },
240
+ configuredReviewers: 2,
241
+ successfulReviewers: 2,
242
+ consensusPolicy: "quorum",
243
+ consensusThreshold: 2,
244
+ panelHealth: "healthy",
245
+ confirmedClusters: [],
246
+ advisories: [],
247
+ reviewers: [
248
+ {
249
+ reviewerId: "r1",
250
+ role: "Independent reviewer",
251
+ model: "openai/gpt",
252
+ durationMs: 1100,
253
+ usage: { input: 450, output: 100, cacheRead: 50, cacheWrite: 0, reasoning: 25, totalTokens: 600, costTotal: 0.0617 },
254
+ status: "clean",
255
+ verdict: "approve",
256
+ verdictSource: "parsed",
257
+ contributed: false,
258
+ },
259
+ ],
260
+ adjudicationUsed: false,
261
+ },
262
+ });
263
+
264
+ const compact = renderPanelResult({ state }, false, theme).render(160).join("\n");
265
+ assert.match(compact, /1\.2K tok/);
266
+ assert.match(compact, /\$0\.1234/);
267
+
268
+ const content = buildPanelResultContent(state);
269
+ // Classic CLI-style ASCII footer (not the old ### Run metrics markdown table).
270
+ assert.match(content, /── pi-review panel/);
271
+ assert.match(content, /Duration\s+1\.2s/);
272
+ assert.match(content, /Tokens\s+.*total 1\.2K|Tokens\s+.*1\.2K total|total:1\.2K|total 1\.2K/);
273
+ assert.match(content, /Cost\s+\$0\.1234|cost:\$0\.1234/);
274
+ assert.match(content, /r1/);
275
+ assert.match(content, /\$0\.0617/);
276
+ });
277
+
278
+ test("panel tool updates and clears ambient footer status during execute", async () => {
279
+ const statuses: Array<string | undefined> = [];
280
+ let registered: { execute: (...args: any[]) => Promise<any> } | undefined;
281
+ registerPanelReviewTool({
282
+ registerTool(tool: { execute: (...args: any[]) => Promise<any> }) {
283
+ registered = tool;
284
+ },
285
+ } as never);
286
+ assert.ok(registered);
287
+
288
+ const result = await registered!.execute(
289
+ "tool-call",
290
+ { target: "@src", reviewers: 1 },
291
+ new AbortController().signal,
292
+ undefined,
293
+ {
294
+ ui: {
295
+ setStatus(key: string, text: string | undefined) {
296
+ assert.equal(key, "pi-review");
297
+ statuses.push(text);
298
+ },
299
+ },
300
+ },
301
+ );
302
+
303
+ assert.equal(result.isError, true);
304
+ // Param errors clear status immediately; never leave a stale footer entry.
305
+ assert.ok(statuses.includes(undefined));
306
+ });
307
+
308
+ test("panel tool content carries full conclusion for the parent LLM", () => {
309
+ const now = Date.now();
310
+ let state = reducePanelEvent(createPanelViewState(), {
311
+ v: 1,
312
+ runId: "run-content",
313
+ seq: 1,
314
+ at: now,
315
+ type: "panel.started",
316
+ target: "@src",
317
+ mode: "code",
318
+ reviewers: [{ reviewerId: "security", role: "Security", model: "openai/gpt", thinking: "high" }],
319
+ });
320
+ state = reducePanelEvent(state, {
321
+ v: 1,
322
+ runId: "run-content",
323
+ seq: 2,
324
+ at: now,
325
+ type: "reviewer.completed",
326
+ reviewerId: "security",
327
+ submission: {
328
+ reviewerId: "security",
329
+ role: "Security",
330
+ model: "openai/gpt",
331
+ thinking: "high",
332
+ durationMs: 100,
333
+ result: {
334
+ status: "has_findings",
335
+ verdict: "request_changes",
336
+ verdictSource: "parsed",
337
+ actionableCount: 1,
338
+ findings: [{ summary: "Auth bypass in session cookie", actionable: true, path: "src/auth.ts" }],
339
+ },
340
+ },
341
+ });
342
+ state = reducePanelEvent(state, {
343
+ v: 1,
344
+ runId: "run-content",
345
+ seq: 3,
346
+ at: now,
347
+ type: "panel.completed",
348
+ meta: {
349
+ strategy: "panel",
350
+ reviewMode: "code",
351
+ status: "has_findings",
352
+ verdict: "request_changes",
353
+ verdictSource: "parsed",
354
+ findings: [],
355
+ actionableCount: 1,
356
+ durationMs: 1200,
357
+ model: "openai/gpt",
358
+ configuredReviewers: 1,
359
+ successfulReviewers: 1,
360
+ consensusPolicy: "quorum",
361
+ consensusThreshold: 1,
362
+ panelHealth: "healthy",
363
+ confirmedClusters: [{ id: "C1", summary: "Auth bypass", supportingReviewerIds: ["security"], sourceFindingIds: ["F1"], confirmed: true, severity: "high", path: "src/auth.ts", supportCount: 1, actionableSupportCount: 1 }],
364
+ advisories: [{ id: "C2", summary: "Naming nit", supportingReviewerIds: ["security"], sourceFindingIds: ["F2"], confirmed: false, supportCount: 1, actionableSupportCount: 0 }],
365
+ reviewers: [{ reviewerId: "security", role: "Security", model: "openai/gpt", thinking: "high", durationMs: 1100, status: "has_findings", verdict: "request_changes", verdictSource: "parsed", contributed: true }],
366
+ adjudicationUsed: false,
367
+ },
368
+ });
369
+
370
+ const content = buildPanelResultContent(state);
371
+ assert.match(content, /── pi-review panel/);
372
+ assert.match(content, /Confirmed findings:/);
373
+ assert.match(content, /Auth bypass/);
374
+ assert.match(content, /Advisories \(non-blocking\):/);
375
+ assert.match(content, /Naming nit/);
376
+ assert.match(content, /Status\s+has findings|HAS FINDINGS|has_findings/);
377
+ assert.match(content, /security/);
378
+ assert.match(content, /openai\/gpt/);
379
+ assert.match(content, /Reviewer summaries:/);
380
+ assert.match(content, /Auth bypass in session cookie/);
381
+ assert.doesNotMatch(content, /### |```/);
382
+
383
+ const expanded = renderPanelResult({ state }, true, theme).render(160).join("\n");
384
+ assert.match(expanded, /── pi-review panel/);
385
+ assert.match(expanded, /Reviewer summaries:/);
386
+ assert.match(expanded, /Auth bypass in session cookie/);
387
+ assert.doesNotMatch(expanded, /### |```/);
30
388
  });