@zephyrdeng/pi-review 0.7.0 → 0.8.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 (194) hide show
  1. package/README.md +51 -7
  2. package/README.zh-CN.md +45 -5
  3. package/dist/args.d.ts +4 -0
  4. package/dist/args.d.ts.map +1 -1
  5. package/dist/args.js +82 -5
  6. package/dist/args.js.map +1 -1
  7. package/dist/args.test.js +72 -1
  8. package/dist/args.test.js.map +1 -1
  9. package/dist/child-process.d.ts +6 -0
  10. package/dist/child-process.d.ts.map +1 -1
  11. package/dist/child-process.js +48 -0
  12. package/dist/child-process.js.map +1 -1
  13. package/dist/child-process.test.js +55 -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 +13 -6
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +9 -5
  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 +34 -2
  29. package/dist/json-events.d.ts.map +1 -1
  30. package/dist/json-events.js +156 -49
  31. package/dist/json-events.js.map +1 -1
  32. package/dist/json-events.test.js +176 -4
  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.d.ts +2 -0
  51. package/dist/panel-cancellation.test.d.ts.map +1 -0
  52. package/dist/panel-cancellation.test.js +55 -0
  53. package/dist/panel-cancellation.test.js.map +1 -0
  54. package/dist/panel-cli-ui.test.d.ts +2 -0
  55. package/dist/panel-cli-ui.test.d.ts.map +1 -0
  56. package/dist/panel-cli-ui.test.js +91 -0
  57. package/dist/panel-cli-ui.test.js.map +1 -0
  58. package/dist/panel-cli.test.js +76 -27
  59. package/dist/panel-cli.test.js.map +1 -1
  60. package/dist/panel-config.d.ts +38 -0
  61. package/dist/panel-config.d.ts.map +1 -1
  62. package/dist/panel-config.js +137 -4
  63. package/dist/panel-config.js.map +1 -1
  64. package/dist/panel-config.test.js +84 -4
  65. package/dist/panel-config.test.js.map +1 -1
  66. package/dist/panel-loop.test.js +1 -1
  67. package/dist/panel-loop.test.js.map +1 -1
  68. package/dist/panel-tools.test.d.ts +2 -0
  69. package/dist/panel-tools.test.d.ts.map +1 -0
  70. package/dist/panel-tools.test.js +12 -0
  71. package/dist/panel-tools.test.js.map +1 -0
  72. package/dist/panel-ui.d.ts +20 -0
  73. package/dist/panel-ui.d.ts.map +1 -0
  74. package/dist/panel-ui.js +84 -0
  75. package/dist/panel-ui.js.map +1 -0
  76. package/dist/panel-ui.test.d.ts +2 -0
  77. package/dist/panel-ui.test.d.ts.map +1 -0
  78. package/dist/panel-ui.test.js +122 -0
  79. package/dist/panel-ui.test.js.map +1 -0
  80. package/dist/panel-usage.d.ts +6 -0
  81. package/dist/panel-usage.d.ts.map +1 -0
  82. package/dist/panel-usage.js +21 -0
  83. package/dist/panel-usage.js.map +1 -0
  84. package/dist/panel-view.d.ts +42 -0
  85. package/dist/panel-view.d.ts.map +1 -0
  86. package/dist/panel-view.js +124 -0
  87. package/dist/panel-view.js.map +1 -0
  88. package/dist/panel-view.test.d.ts +2 -0
  89. package/dist/panel-view.test.d.ts.map +1 -0
  90. package/dist/panel-view.test.js +112 -0
  91. package/dist/panel-view.test.js.map +1 -0
  92. package/dist/panel.d.ts +22 -2
  93. package/dist/panel.d.ts.map +1 -1
  94. package/dist/panel.js +184 -60
  95. package/dist/panel.js.map +1 -1
  96. package/dist/prompt.d.ts +2 -0
  97. package/dist/prompt.d.ts.map +1 -1
  98. package/dist/prompt.js +34 -3
  99. package/dist/prompt.js.map +1 -1
  100. package/dist/prompt.test.js +21 -2
  101. package/dist/prompt.test.js.map +1 -1
  102. package/dist/review-events.d.ts +70 -0
  103. package/dist/review-events.d.ts.map +1 -0
  104. package/dist/review-events.js +52 -0
  105. package/dist/review-events.js.map +1 -0
  106. package/dist/review-events.test.d.ts +2 -0
  107. package/dist/review-events.test.d.ts.map +1 -0
  108. package/dist/review-events.test.js +47 -0
  109. package/dist/review-events.test.js.map +1 -0
  110. package/dist/review-result.test.js +1 -1
  111. package/dist/review-result.test.js.map +1 -1
  112. package/dist/review.js +3 -3
  113. package/dist/review.js.map +1 -1
  114. package/dist/review.meta.test.js +1 -1
  115. package/dist/review.meta.test.js.map +1 -1
  116. package/dist/skill-protocol.test.js +1 -1
  117. package/dist/skill-protocol.test.js.map +1 -1
  118. package/dist/skill-update.test.d.ts +2 -0
  119. package/dist/skill-update.test.d.ts.map +1 -0
  120. package/dist/skill-update.test.js +23 -0
  121. package/dist/skill-update.test.js.map +1 -0
  122. package/dist/skill.d.ts +22 -0
  123. package/dist/skill.d.ts.map +1 -1
  124. package/dist/skill.js +109 -22
  125. package/dist/skill.js.map +1 -1
  126. package/dist/types.d.ts +28 -2
  127. package/dist/types.d.ts.map +1 -1
  128. package/dist/types.js +2 -0
  129. package/dist/types.js.map +1 -1
  130. package/dist/ui-client.d.ts +13 -0
  131. package/dist/ui-client.d.ts.map +1 -0
  132. package/dist/ui-client.js +161 -0
  133. package/dist/ui-client.js.map +1 -0
  134. package/dist/ui-client.test.d.ts +2 -0
  135. package/dist/ui-client.test.d.ts.map +1 -0
  136. package/dist/ui-client.test.js +48 -0
  137. package/dist/ui-client.test.js.map +1 -0
  138. package/dist/ui-html.d.ts +7 -0
  139. package/dist/ui-html.d.ts.map +1 -0
  140. package/dist/ui-html.js +52 -0
  141. package/dist/ui-html.js.map +1 -0
  142. package/dist/ui-html.test.d.ts +2 -0
  143. package/dist/ui-html.test.d.ts.map +1 -0
  144. package/dist/ui-html.test.js +26 -0
  145. package/dist/ui-html.test.js.map +1 -0
  146. package/dist/ui-server-main.d.ts +3 -0
  147. package/dist/ui-server-main.d.ts.map +1 -0
  148. package/dist/ui-server-main.js +128 -0
  149. package/dist/ui-server-main.js.map +1 -0
  150. package/dist/ui-server-main.test.d.ts +2 -0
  151. package/dist/ui-server-main.test.d.ts.map +1 -0
  152. package/dist/ui-server-main.test.js +123 -0
  153. package/dist/ui-server-main.test.js.map +1 -0
  154. package/dist/ui-server.d.ts +42 -0
  155. package/dist/ui-server.d.ts.map +1 -0
  156. package/dist/ui-server.js +233 -0
  157. package/dist/ui-server.js.map +1 -0
  158. package/dist/ui-server.test.d.ts +2 -0
  159. package/dist/ui-server.test.d.ts.map +1 -0
  160. package/dist/ui-server.test.js +236 -0
  161. package/dist/ui-server.test.js.map +1 -0
  162. package/dist/update.d.ts.map +1 -1
  163. package/dist/update.js +48 -19
  164. package/dist/update.js.map +1 -1
  165. package/extensions/panel-tool.test.ts +388 -0
  166. package/extensions/panel-tool.ts +390 -0
  167. package/extensions/review.test.ts +79 -0
  168. package/extensions/review.ts +191 -45
  169. package/extensions/rv-completions.test.ts +177 -18
  170. package/extensions/rv-completions.ts +490 -60
  171. package/extensions/rv-interactive.test.ts +562 -0
  172. package/extensions/rv-interactive.ts +616 -0
  173. package/extensions/rv-locale.test.ts +12 -4
  174. package/extensions/rv-locale.ts +44 -11
  175. package/extensions/rv-model-picker.test.ts +172 -0
  176. package/extensions/rv-model-picker.ts +313 -0
  177. package/extensions/rv-model-priorities.test.ts +29 -9
  178. package/extensions/rv-model-priorities.ts +23 -8
  179. package/extensions/rv-prompts.test.ts +281 -2
  180. package/extensions/rv-prompts.ts +480 -32
  181. package/extensions/rv-resolve.test.ts +189 -0
  182. package/extensions/rv-resolve.ts +313 -0
  183. package/extensions/rv-semantic.test.ts +1 -1
  184. package/extensions/rv-semantic.ts +38 -4
  185. package/package.json +18 -6
  186. package/resources/review-presets.json +2 -2
  187. package/resources/rv-model-priorities.json +18 -9
  188. package/skills/pi-review/SKILL.md +84 -60
  189. package/skills/pi-review/references/codex-tools.md +18 -11
  190. package/skills/pi-review/references/model-selection.md +49 -28
  191. package/dist/extensions/review.d.ts +0 -2
  192. package/dist/extensions/review.js +0 -22
  193. package/dist/extensions/rv-prompts.d.ts +0 -19
  194. package/dist/extensions/rv-prompts.js +0 -132
@@ -0,0 +1,388 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "vitest";
3
+ import { createPanelViewState, reducePanelEvent } from "@zephyrdeng/pi-review";
4
+ import {
5
+ buildPanelResultContent,
6
+ expandHint,
7
+ panelToolParamError,
8
+ PanelResultView,
9
+ registerPanelReviewTool,
10
+ renderPanelResult,
11
+ } from "./panel-tool.js";
12
+
13
+ const theme = {
14
+ fg: (_color: "toolTitle" | "muted" | "error" | "success" | "accent", text: string) => text,
15
+ bold: (text: string) => text,
16
+ };
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
+
43
+ test("Pi panel renderer exposes compact reviewer progress and expanded activity", () => {
44
+ const now = Date.now();
45
+ const started = reducePanelEvent(createPanelViewState(), {
46
+ v: 1,
47
+ runId: "run-1",
48
+ seq: 1,
49
+ at: now,
50
+ type: "panel.started",
51
+ target: "@src",
52
+ mode: "code",
53
+ reviewers: [{ reviewerId: "security", role: "Security", model: "openai/gpt", thinking: "high" }],
54
+ });
55
+ const reviewerStarted = reducePanelEvent(started, { v: 1, runId: "run-1", seq: 2, at: now, type: "reviewer.started", reviewerId: "security" });
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);
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, /### |```/);
388
+ });