@zephyrdeng/pi-review 0.7.1 → 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 (176) 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.map +1 -1
  146. package/dist/update.js +48 -19
  147. package/dist/update.js.map +1 -1
  148. package/extensions/panel-tool.test.ts +366 -8
  149. package/extensions/panel-tool.ts +289 -68
  150. package/extensions/review.test.ts +79 -0
  151. package/extensions/review.ts +188 -44
  152. package/extensions/rv-completions.test.ts +177 -18
  153. package/extensions/rv-completions.ts +490 -60
  154. package/extensions/rv-interactive.test.ts +562 -0
  155. package/extensions/rv-interactive.ts +616 -0
  156. package/extensions/rv-locale.test.ts +12 -4
  157. package/extensions/rv-locale.ts +44 -11
  158. package/extensions/rv-model-picker.test.ts +172 -0
  159. package/extensions/rv-model-picker.ts +313 -0
  160. package/extensions/rv-model-priorities.test.ts +29 -9
  161. package/extensions/rv-model-priorities.ts +23 -8
  162. package/extensions/rv-prompts.test.ts +274 -2
  163. package/extensions/rv-prompts.ts +479 -33
  164. package/extensions/rv-resolve.test.ts +189 -0
  165. package/extensions/rv-resolve.ts +313 -0
  166. package/extensions/rv-semantic.test.ts +1 -1
  167. package/extensions/rv-semantic.ts +38 -4
  168. package/package.json +7 -4
  169. package/resources/rv-model-priorities.json +18 -9
  170. package/skills/pi-review/SKILL.md +84 -60
  171. package/skills/pi-review/references/codex-tools.md +18 -11
  172. package/skills/pi-review/references/model-selection.md +49 -28
  173. package/dist/extensions/review.d.ts +0 -2
  174. package/dist/extensions/review.js +0 -22
  175. package/dist/extensions/rv-prompts.d.ts +0 -19
  176. package/dist/extensions/rv-prompts.js +0 -132
@@ -1,4 +1,4 @@
1
- /** /rv → parent-agent task text; child review uses resources/review-presets.json */
1
+ /** /rv* → parent-agent task text; strategy lives on the command, target stays natural language. */
2
2
 
3
3
  import type { RvLocale } from "./rv-locale.js";
4
4
  import { mergeSemanticIntoParsed, stripSemanticPhrases } from "./rv-semantic.js";
@@ -6,9 +6,35 @@ import { mergeSemanticIntoParsed, stripSemanticPhrases } from "./rv-semantic.js"
6
6
  export const RV_MODES = ["code", "plan", "challenge"] as const;
7
7
  export type RvMode = (typeof RV_MODES)[number];
8
8
 
9
- const FLAGS_REQUIRING_VALUE = ["--mode", "--model", "--thinking", "--continue"] as const;
9
+ /** Slash-command strategy. Target text after the command is always natural language. */
10
+ export const RV_STRATEGIES = ["panel", "loop", "models"] as const;
11
+ export type RvStrategy = (typeof RV_STRATEGIES)[number];
12
+
13
+ const RV_ARG_TOKEN_RE = /(?:[^\s"']+|"[^"]*"|'[^']*')+/g;
14
+
15
+ const FLAGS_REQUIRING_VALUE = [
16
+ "--mode",
17
+ "--model",
18
+ "--thinking",
19
+ "--continue",
20
+ "--max-rounds",
21
+ "--until",
22
+ "--reviewers",
23
+ "--panel",
24
+ "--reviewer-model",
25
+ "--consensus",
26
+ "--min-agree",
27
+ "--consensus-model",
28
+ "--concurrency",
29
+ ] as const;
30
+
31
+ export const RV_CONSENSUS_POLICIES = ["any", "quorum", "majority", "unanimous"] as const;
32
+ export type RvConsensusPolicy = (typeof RV_CONSENSUS_POLICIES)[number];
33
+ export const RV_MAX_REVIEWERS = 8;
10
34
 
11
35
  export type RvParsed = {
36
+ /** Selected by the slash command (/rv, /rv-loop, /rv-models), not by rewriting the target. */
37
+ strategy: RvStrategy;
12
38
  mode: string;
13
39
  model?: string;
14
40
  thinking?: string;
@@ -16,16 +42,82 @@ export type RvParsed = {
16
42
  noStream: boolean;
17
43
  continueHandle?: string;
18
44
  modelsOnly: boolean;
45
+ /** Optional loop budget when strategy is loop. */
46
+ maxRounds?: number;
47
+ /** Set when --max-rounds was present but not a strict positive safe integer. */
48
+ maxRoundsError?: string;
49
+ /** Loop stop goal. Only `clean` is supported. */
50
+ until?: "clean";
51
+ untilError?: string;
52
+ /** Panel width: independent reviewers for one gate (and each loop round). */
53
+ reviewers?: number;
54
+ reviewersError?: string;
55
+ /** Named panel preset (e.g. code-experts). Cannot combine with --reviewers. */
56
+ panel?: string;
57
+ /** Per-reviewer model overrides: id=provider/model (repeatable). */
58
+ reviewerModels?: string[];
59
+ reviewerModelsError?: string;
60
+ consensus?: RvConsensusPolicy;
61
+ consensusError?: string;
62
+ minAgree?: number;
63
+ minAgreeError?: string;
64
+ consensusModel?: string;
65
+ concurrency?: number;
66
+ concurrencyError?: string;
67
+ /** Set when a known value-taking flag was present without a value. */
68
+ missingValueError?: string;
69
+ /** Natural-language review request exactly as the user typed it. */
19
70
  target: string;
20
71
  };
21
72
 
73
+ /** Same strict rule as CLI `parsePositiveInteger`: /^[1-9]\d*$/ and safe integer. */
74
+ export function parseStrictPositiveSafeInteger(flag: string, value: string): number {
75
+ if (!/^[1-9]\d*$/.test(value)) {
76
+ throw new Error(`${flag} must be a positive integer`);
77
+ }
78
+ const parsed = Number(value);
79
+ if (!Number.isSafeInteger(parsed)) {
80
+ throw new Error(`${flag} must be a safe positive integer`);
81
+ }
82
+ return parsed;
83
+ }
84
+
85
+ /** /rv-loop host fix-point default (skill closeout), not the CLI loop default of 3. */
86
+ export const RV_LOOP_DEFAULT_MAX_ROUNDS = 1;
87
+ /** Host-cycle budget when /rv-loop --until clean is set without an explicit --max-rounds. */
88
+ export const RV_LOOP_UNTIL_CLEAN_DEFAULT_MAX_ROUNDS = 10;
89
+
90
+ /** Shared clean-goal definition for /rv-loop --until clean. */
91
+ export const RV_CLEAN_GOAL = {
92
+ id: "clean" as const,
93
+ summary:
94
+ "status=clean: no gate-blocking findings (single: no actionable findings; panel: no confirmed actionable clusters; advisories may remain)",
95
+ hostProtocol: [
96
+ "Until-clean host protocol (you are the only editor):",
97
+ "1. Goal: status=clean and exit 0. Advisories may remain; confirmed/actionable findings do not.",
98
+ "2. Run one review gate (CLI may still use --max-rounds as a hard ceiling inside that process).",
99
+ "3. If clean → stop and close out with proof.",
100
+ "4. If has_findings → fix only accepted in-scope blockers, rerun proof, then re-invoke /rv-loop --until clean.",
101
+ "5. If needs_human/blocked → escalate immediately; never claim clean.",
102
+ "6. Hard budget: stop after max-rounds host cycles (default 10 with --until clean) and report remaining findings.",
103
+ "7. Never pretend the child can edit. Never loop forever on an unchanged tree.",
104
+ ].join("\n"),
105
+ } as const;
106
+
22
107
  export type RvValidation =
23
108
  | { ok: true }
24
109
  | { ok: false; message: string };
25
110
 
26
- export function parseRvArgs(raw: string): RvParsed {
27
- const { remainder, apply: semanticApply } = stripSemanticPhrases(raw);
28
- const tokens = remainder.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
111
+ export function parseRvArgs(raw: string, strategy: RvStrategy = "panel"): RvParsed {
112
+ // `--` is the completion-friendly boundary between options and free text.
113
+ // Semantic shortcuts only apply before it, preserving the target typed last.
114
+ const rawTokens: string[] = raw.match(RV_ARG_TOKEN_RE) ?? [];
115
+ const terminatorIndex = rawTokens.indexOf("--");
116
+ const optionRaw = (terminatorIndex >= 0 ? rawTokens.slice(0, terminatorIndex) : rawTokens).join(" ");
117
+ const explicitTarget = (terminatorIndex >= 0 ? rawTokens.slice(terminatorIndex + 1) : [])
118
+ .map((t) => t.replace(/^['"]|['"]$/g, ""));
119
+ const { remainder, apply: semanticApply } = stripSemanticPhrases(optionRaw);
120
+ const tokens = remainder.match(RV_ARG_TOKEN_RE) ?? [];
29
121
  const cleaned = tokens.map((t) => t.replace(/^['"]|['"]$/g, ""));
30
122
 
31
123
  let mode = "code";
@@ -34,15 +126,50 @@ export function parseRvArgs(raw: string): RvParsed {
34
126
  let keepSession = false;
35
127
  let noStream = false;
36
128
  let continueHandle: string | undefined;
37
- let modelsOnly = false;
129
+ let modelsOnly = strategy === "models";
130
+ let maxRounds: number | undefined;
131
+ let maxRoundsError: string | undefined;
132
+ let until: "clean" | undefined;
133
+ let untilError: string | undefined;
134
+ let reviewers: number | undefined;
135
+ let reviewersError: string | undefined;
136
+ let panel: string | undefined;
137
+ let reviewerModels: string[] | undefined;
138
+ let reviewerModelsError: string | undefined;
139
+ let consensus: RvConsensusPolicy | undefined;
140
+ let consensusError: string | undefined;
141
+ let minAgree: number | undefined;
142
+ let minAgreeError: string | undefined;
143
+ let consensusModel: string | undefined;
144
+ let concurrency: number | undefined;
145
+ let concurrencyError: string | undefined;
146
+ let missingValueError: string | undefined;
38
147
  const rest: string[] = [];
148
+ const markMissingValue = (flag: string) => {
149
+ missingValueError ??= `Missing value for ${flag}.`;
150
+ };
39
151
 
40
152
  for (let i = 0; i < cleaned.length; i++) {
41
153
  const t = cleaned[i];
42
- if (t === "models" && rest.length === 0 && i === cleaned.length - 1) {
154
+ // Conventional option terminator: everything after `--` is target text.
155
+ if (t === "--") {
156
+ rest.push(...cleaned.slice(i + 1));
157
+ break;
158
+ }
159
+ // Backward compatible: `/rv models` still works, but `/rv-models` is preferred.
160
+ if (strategy === "panel" && t === "models" && rest.length === 0 && i === cleaned.length - 1) {
43
161
  modelsOnly = true;
44
162
  continue;
45
163
  }
164
+ // An @path is always review-target syntax, never a flag value. Reject the
165
+ // preceding flag as missing instead of swallowing the target as its value.
166
+ if ((FLAGS_REQUIRING_VALUE as readonly string[]).includes(t)) {
167
+ const next = cleaned[i + 1];
168
+ if (!next || next.startsWith("--") || next.startsWith("@")) {
169
+ markMissingValue(t);
170
+ continue;
171
+ }
172
+ }
46
173
  if (t === "--keep-session") {
47
174
  keepSession = true;
48
175
  continue;
@@ -54,7 +181,7 @@ export function parseRvArgs(raw: string): RvParsed {
54
181
  if (t === "--mode") {
55
182
  const m = cleaned[i + 1];
56
183
  if (!m || m.startsWith("--")) {
57
- rest.push(t);
184
+ markMissingValue(t);
58
185
  continue;
59
186
  }
60
187
  mode = cleaned[++i];
@@ -63,7 +190,7 @@ export function parseRvArgs(raw: string): RvParsed {
63
190
  if (t === "--model") {
64
191
  const v = cleaned[i + 1];
65
192
  if (!v || v.startsWith("--")) {
66
- rest.push(t);
193
+ markMissingValue(t);
67
194
  continue;
68
195
  }
69
196
  model = cleaned[++i];
@@ -72,7 +199,7 @@ export function parseRvArgs(raw: string): RvParsed {
72
199
  if (t === "--thinking") {
73
200
  const v = cleaned[i + 1];
74
201
  if (!v || v.startsWith("--")) {
75
- rest.push(t);
202
+ markMissingValue(t);
76
203
  continue;
77
204
  }
78
205
  thinking = cleaned[++i];
@@ -81,17 +208,150 @@ export function parseRvArgs(raw: string): RvParsed {
81
208
  if (t === "--continue") {
82
209
  const v = cleaned[i + 1];
83
210
  if (!v || v.startsWith("--")) {
84
- rest.push(t);
211
+ markMissingValue(t);
85
212
  continue;
86
213
  }
87
214
  continueHandle = cleaned[++i];
88
215
  continue;
89
216
  }
217
+ if (t === "--max-rounds") {
218
+ const v = cleaned[i + 1];
219
+ if (!v || v.startsWith("--")) {
220
+ markMissingValue(t);
221
+ continue;
222
+ }
223
+ const raw = cleaned[++i]!;
224
+ try {
225
+ maxRounds = parseStrictPositiveSafeInteger("--max-rounds", raw);
226
+ maxRoundsError = undefined;
227
+ } catch (error) {
228
+ maxRounds = undefined;
229
+ maxRoundsError = error instanceof Error ? error.message : "--max-rounds must be a positive integer";
230
+ }
231
+ continue;
232
+ }
233
+ if (t === "--until") {
234
+ const v = cleaned[i + 1];
235
+ if (!v || v.startsWith("--")) {
236
+ markMissingValue(t);
237
+ continue;
238
+ }
239
+ const raw = cleaned[++i]!;
240
+ if (raw === "clean") {
241
+ until = "clean";
242
+ untilError = undefined;
243
+ } else {
244
+ until = undefined;
245
+ untilError = `--until only supports clean (${RV_CLEAN_GOAL.summary})`;
246
+ }
247
+ continue;
248
+ }
249
+ if (t === "--reviewers") {
250
+ const v = cleaned[i + 1];
251
+ if (!v || v.startsWith("--")) {
252
+ markMissingValue(t);
253
+ continue;
254
+ }
255
+ const raw = cleaned[++i]!;
256
+ try {
257
+ reviewers = parseStrictPositiveSafeInteger("--reviewers", raw);
258
+ reviewersError = undefined;
259
+ } catch (error) {
260
+ reviewers = undefined;
261
+ reviewersError = error instanceof Error ? error.message : "--reviewers must be a positive integer";
262
+ }
263
+ continue;
264
+ }
265
+ if (t === "--panel") {
266
+ const v = cleaned[i + 1];
267
+ if (!v || v.startsWith("--")) {
268
+ markMissingValue(t);
269
+ continue;
270
+ }
271
+ panel = cleaned[++i];
272
+ continue;
273
+ }
274
+ if (t === "--reviewer-model") {
275
+ const v = cleaned[i + 1];
276
+ if (!v || v.startsWith("--")) {
277
+ markMissingValue(t);
278
+ continue;
279
+ }
280
+ const raw = cleaned[++i]!;
281
+ if (!/^[A-Za-z0-9_.-]+\S*=\S+$/.test(raw) && !/^[A-Za-z0-9_.-]+=\S+$/.test(raw)) {
282
+ reviewerModelsError = "--reviewer-model must look like id=provider/model (e.g. r1=openai/gpt-5.6-sol)";
283
+ } else {
284
+ reviewerModels = [...(reviewerModels ?? []), raw];
285
+ }
286
+ continue;
287
+ }
288
+ if (t === "--consensus") {
289
+ const v = cleaned[i + 1];
290
+ if (!v || v.startsWith("--")) {
291
+ markMissingValue(t);
292
+ continue;
293
+ }
294
+ const raw = cleaned[++i]!;
295
+ if ((RV_CONSENSUS_POLICIES as readonly string[]).includes(raw)) {
296
+ consensus = raw as RvConsensusPolicy;
297
+ consensusError = undefined;
298
+ } else {
299
+ consensus = undefined;
300
+ consensusError = `--consensus must be one of ${RV_CONSENSUS_POLICIES.join(" | ")}`;
301
+ }
302
+ continue;
303
+ }
304
+ if (t === "--min-agree") {
305
+ const v = cleaned[i + 1];
306
+ if (!v || v.startsWith("--")) {
307
+ markMissingValue(t);
308
+ continue;
309
+ }
310
+ const raw = cleaned[++i]!;
311
+ try {
312
+ minAgree = parseStrictPositiveSafeInteger("--min-agree", raw);
313
+ minAgreeError = undefined;
314
+ } catch (error) {
315
+ minAgree = undefined;
316
+ minAgreeError = error instanceof Error ? error.message : "--min-agree must be a positive integer";
317
+ }
318
+ continue;
319
+ }
320
+ if (t === "--consensus-model") {
321
+ const v = cleaned[i + 1];
322
+ if (!v || v.startsWith("--")) {
323
+ markMissingValue(t);
324
+ continue;
325
+ }
326
+ consensusModel = cleaned[++i];
327
+ continue;
328
+ }
329
+ if (t === "--concurrency") {
330
+ const v = cleaned[i + 1];
331
+ if (!v || v.startsWith("--")) {
332
+ markMissingValue(t);
333
+ continue;
334
+ }
335
+ const raw = cleaned[++i]!;
336
+ try {
337
+ concurrency = parseStrictPositiveSafeInteger("--concurrency", raw);
338
+ concurrencyError = undefined;
339
+ } catch (error) {
340
+ concurrency = undefined;
341
+ concurrencyError = error instanceof Error ? error.message : "--concurrency must be a positive integer";
342
+ }
343
+ continue;
344
+ }
90
345
  rest.push(t);
91
346
  }
92
347
 
348
+ // Text after the explicit separator must not be interpreted as strategy syntax.
349
+ rest.push(...explicitTarget);
350
+
351
+ const effectiveStrategy: RvStrategy = modelsOnly ? "models" : strategy;
93
352
  return mergeSemanticIntoParsed(
94
353
  {
354
+ strategy: effectiveStrategy,
95
355
  mode,
96
356
  model,
97
357
  thinking,
@@ -99,6 +359,23 @@ export function parseRvArgs(raw: string): RvParsed {
99
359
  noStream,
100
360
  continueHandle,
101
361
  modelsOnly,
362
+ ...(maxRounds !== undefined ? { maxRounds } : {}),
363
+ ...(maxRoundsError ? { maxRoundsError } : {}),
364
+ ...(until ? { until } : {}),
365
+ ...(untilError ? { untilError } : {}),
366
+ ...(reviewers !== undefined ? { reviewers } : {}),
367
+ ...(reviewersError ? { reviewersError } : {}),
368
+ ...(panel ? { panel } : {}),
369
+ ...(reviewerModels ? { reviewerModels } : {}),
370
+ ...(reviewerModelsError ? { reviewerModelsError } : {}),
371
+ ...(consensus ? { consensus } : {}),
372
+ ...(consensusError ? { consensusError } : {}),
373
+ ...(minAgree !== undefined ? { minAgree } : {}),
374
+ ...(minAgreeError ? { minAgreeError } : {}),
375
+ ...(consensusModel ? { consensusModel } : {}),
376
+ ...(concurrency !== undefined ? { concurrency } : {}),
377
+ ...(concurrencyError ? { concurrencyError } : {}),
378
+ ...(missingValueError ? { missingValueError } : {}),
102
379
  target: rest.join(" ").trim(),
103
380
  },
104
381
  semanticApply,
@@ -106,9 +383,52 @@ export function parseRvArgs(raw: string): RvParsed {
106
383
  }
107
384
 
108
385
  export function validateRvParsed(parsed: RvParsed): RvValidation {
386
+ if (parsed.strategy === "loop" && (parsed.keepSession || parsed.continueHandle)) {
387
+ return { ok: false, message: "/rv-loop does not support --keep-session or --continue." };
388
+ }
109
389
  if (parsed.keepSession && parsed.continueHandle) {
110
390
  return { ok: false, message: "--keep-session and --continue cannot be used together." };
111
391
  }
392
+ if (parsed.missingValueError) return { ok: false, message: parsed.missingValueError };
393
+ if (parsed.maxRoundsError) return { ok: false, message: parsed.maxRoundsError };
394
+ if (parsed.untilError) return { ok: false, message: parsed.untilError };
395
+ if (parsed.reviewersError) return { ok: false, message: parsed.reviewersError };
396
+ if (parsed.reviewerModelsError) return { ok: false, message: parsed.reviewerModelsError };
397
+ if (parsed.consensusError) return { ok: false, message: parsed.consensusError };
398
+ if (parsed.minAgreeError) return { ok: false, message: parsed.minAgreeError };
399
+ if (parsed.concurrencyError) return { ok: false, message: parsed.concurrencyError };
400
+ if (parsed.maxRounds !== undefined && parsed.strategy !== "loop") {
401
+ return { ok: false, message: "--max-rounds is only valid with /rv-loop." };
402
+ }
403
+ if (parsed.until !== undefined && parsed.strategy !== "loop") {
404
+ return { ok: false, message: "--until is only valid with /rv-loop." };
405
+ }
406
+ if (parsed.reviewers !== undefined && parsed.panel) {
407
+ return { ok: false, message: "--reviewers cannot be used with --panel" };
408
+ }
409
+ if (parsed.reviewers !== undefined && (parsed.reviewers < 1 || parsed.reviewers > RV_MAX_REVIEWERS)) {
410
+ return { ok: false, message: `--reviewers must be between 1 and ${RV_MAX_REVIEWERS}` };
411
+ }
412
+ const hasPanelOnlyOptions =
413
+ parsed.consensus !== undefined ||
414
+ parsed.minAgree !== undefined ||
415
+ parsed.consensusModel !== undefined ||
416
+ parsed.concurrency !== undefined ||
417
+ (parsed.reviewerModels?.length ?? 0) > 0;
418
+ if (parsed.reviewers === 1 && hasPanelOnlyOptions) {
419
+ return { ok: false, message: "panel options require --reviewers > 1 or --panel" };
420
+ }
421
+ if (parsed.minAgree !== undefined) {
422
+ if (parsed.consensus && parsed.consensus !== "quorum") {
423
+ return { ok: false, message: "--min-agree is only valid with --consensus quorum" };
424
+ }
425
+ if (parsed.reviewers !== undefined && parsed.minAgree > parsed.reviewers) {
426
+ return { ok: false, message: `--min-agree ${parsed.minAgree} cannot exceed reviewers ${parsed.reviewers}` };
427
+ }
428
+ }
429
+ if (parsed.concurrency !== undefined && parsed.reviewers !== undefined && parsed.concurrency > parsed.reviewers) {
430
+ return { ok: false, message: `--concurrency ${parsed.concurrency} cannot exceed reviewers ${parsed.reviewers}` };
431
+ }
112
432
  for (const flag of FLAGS_REQUIRING_VALUE) {
113
433
  if (parsed.target === flag) {
114
434
  return { ok: false, message: `Missing value for ${flag}.` };
@@ -117,10 +437,31 @@ export function validateRvParsed(parsed: RvParsed): RvValidation {
117
437
  return { ok: true };
118
438
  }
119
439
 
440
+ /** Shell-safe single argument for Execute: lines shown to the parent agent. */
441
+ export function shellQuote(value: string): string {
442
+ if (value === "") return "''";
443
+ // Prefer single quotes; only escape embedded single quotes.
444
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
445
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
446
+ }
447
+
120
448
  /** Same optional flags for initial review and --continue follow-up (all optional). */
121
449
  export function buildPiReviewArgv(parsed: RvParsed, target: string): string[] {
122
450
  const parts = ["pi-review"];
123
- if (parsed.continueHandle) {
451
+ if (parsed.strategy === "loop") {
452
+ parts.push("loop");
453
+ if (parsed.until === "clean") {
454
+ parts.push("--until", "clean");
455
+ // Until-clean always carries an explicit hard budget (never unlimited).
456
+ parts.push(
457
+ "--max-rounds",
458
+ String(parsed.maxRounds ?? RV_LOOP_UNTIL_CLEAN_DEFAULT_MAX_ROUNDS),
459
+ );
460
+ } else {
461
+ // Host fix-point default for /rv-loop is one gate; CLI `pi-review loop` still defaults to 3.
462
+ parts.push("--max-rounds", String(parsed.maxRounds ?? RV_LOOP_DEFAULT_MAX_ROUNDS));
463
+ }
464
+ } else if (parsed.continueHandle) {
124
465
  parts.push("--continue", parsed.continueHandle);
125
466
  } else if (parsed.keepSession) {
126
467
  parts.push("--keep-session");
@@ -129,10 +470,44 @@ export function buildPiReviewArgv(parsed: RvParsed, target: string): string[] {
129
470
  if (parsed.model) parts.push("--model", parsed.model);
130
471
  if (parsed.thinking) parts.push("--thinking", parsed.thinking);
131
472
  if (parsed.noStream) parts.push("--no-stream");
473
+ if (parsed.panel) parts.push("--panel", parsed.panel);
474
+ if (parsed.reviewers !== undefined) parts.push("--reviewers", String(parsed.reviewers));
475
+ for (const mapping of parsed.reviewerModels ?? []) parts.push("--reviewer-model", mapping);
476
+ if (parsed.consensus) parts.push("--consensus", parsed.consensus);
477
+ if (parsed.minAgree !== undefined) parts.push("--min-agree", String(parsed.minAgree));
478
+ if (parsed.consensusModel) parts.push("--consensus-model", parsed.consensusModel);
479
+ if (parsed.concurrency !== undefined) parts.push("--concurrency", String(parsed.concurrency));
132
480
  parts.push("--", target);
133
481
  return parts;
134
482
  }
135
483
 
484
+ /** Format argv as a shell-safe command line for orchestration prompts. */
485
+ export function formatPiReviewCommandLine(parsed: RvParsed, target: string): string {
486
+ return buildPiReviewArgv(parsed, target).map(shellQuote).join(" ");
487
+ }
488
+
489
+ /** Build the native pi_review tool call instruction without dropping panel config. */
490
+ export function buildPiReviewToolCallInstruction(parsed: RvParsed, target: string): string {
491
+ const fields: string[] = [
492
+ `target=${JSON.stringify(target)}`,
493
+ `mode=${JSON.stringify(parsed.mode)}`,
494
+ ];
495
+ if (parsed.panel) fields.push(`panel=${JSON.stringify(parsed.panel)}`);
496
+ if (parsed.reviewers !== undefined) fields.push(`reviewers=${parsed.reviewers}`);
497
+ if (parsed.reviewerModels?.length) fields.push(`reviewerModels=${JSON.stringify(parsed.reviewerModels)}`);
498
+ if (parsed.consensus) fields.push(`consensus=${JSON.stringify(parsed.consensus)}`);
499
+ if (parsed.minAgree !== undefined) fields.push(`minAgree=${parsed.minAgree}`);
500
+ if (parsed.consensusModel) fields.push(`consensusModel=${JSON.stringify(parsed.consensusModel)}`);
501
+ if (parsed.concurrency !== undefined) fields.push(`concurrency=${parsed.concurrency}`);
502
+ if (parsed.model) fields.push(`model=${JSON.stringify(parsed.model)}`);
503
+ if (parsed.thinking) fields.push(`thinking=${JSON.stringify(parsed.thinking)}`);
504
+ // Default panel when /rv has no explicit width/preset: keep code-experts.
505
+ if (parsed.strategy === "panel" && !parsed.panel && parsed.reviewers === undefined) {
506
+ fields.push(`panel=${JSON.stringify("code-experts")}`);
507
+ }
508
+ return `Call pi_review with ${fields.join(", ")}. Treat target as a natural-language review request. Do not expand directory targets into multi-file lists. Do not drop panel/reviewers/reviewerModels/consensus fields.`;
509
+ }
510
+
136
511
  export function orchestrationLocaleNote(locale: RvLocale = "en"): string {
137
512
  if (locale === "zh") {
138
513
  return "用中文向用户总结(模型目录、选型理由、review 结论);技术 id 如 provider/model 可保留英文。";
@@ -140,27 +515,38 @@ export function orchestrationLocaleNote(locale: RvLocale = "en"): string {
140
515
  return "Summarize for the user in English (catalog, model rationale, review conclusion); keep technical ids such as provider/model as listed.";
141
516
  }
142
517
 
143
- function piHostRules(): string {
518
+ function piHostRules(strategy: RvStrategy): string {
144
519
  return [
145
520
  "Follow the pi-review skill.",
146
- "Host: Pi interactive session (/rv).",
147
- "For new review runs, call the pi_review custom tool. It launches pi-review in isolated reviewer sessions and renders panel progress live.",
521
+ "Host: Pi interactive session (/rv*).",
522
+ "Slash commands select strategy only: /rv = panel review, /rv-loop = loop closeout, /rv-models = model catalog.",
523
+ "Everything after the command is a natural-language review request. Path mentions like @src are fine as text.",
524
+ "Do not expand directory targets into multi-file lists. Strategy matching beyond the slash command (mode defaults, model choice, panel preset, path-vs-file handling) lives in the pi-review skill and CLI.",
525
+ strategy === "panel"
526
+ ? "For /rv panel runs, call the pi_review custom tool. Exception: explicit --reviewers 1 is a non-panel single review and must use the shell CLI path; never replace it with the default code-experts panel."
527
+ : strategy === "loop"
528
+ ? "For /rv-loop, follow the Loop closeout protocol in the pi-review skill. Use the shell CLI loop path; do not pretend the child can edit files."
529
+ : "For /rv-models, only list models; do not start a review.",
148
530
  "CLI defaults: stream child output live. Do NOT add --no-stream or --progress-log unless the user explicitly asked.",
149
531
  "Do not edit, patch, commit, or implement findings unless the user asks separately.",
150
- "For pi_review panel runs, report the tool's rendered panel result and status. For CLI fallback and --continue runs, show the ASCII pi-review footer (lines starting with ── pi-review). Do not paste PI_REVIEW_META_JSON to the user; use Session from a CLI footer for /rv --continue.",
532
+ "For pi_review panel runs, report the tool's rendered panel result and status. For CLI fallback, loop, and --continue runs, show the ASCII pi-review footer (lines starting with ── pi-review). Do not paste PI_REVIEW_META_JSON to the user; use Session from a CLI footer for /rv --continue.",
151
533
  ].join(" ");
152
534
  }
153
535
 
154
- export function buildRvOrchestrationPrompt(parsed: RvParsed, locale: RvLocale = "en"): string {
536
+ export function buildRvOrchestrationPrompt(
537
+ parsed: RvParsed,
538
+ locale: RvLocale = "en",
539
+ resolutionNotes: string[] = [],
540
+ ): string {
155
541
  const localeNote = orchestrationLocaleNote(locale);
156
- if (parsed.modelsOnly) {
542
+ if (parsed.modelsOnly || parsed.strategy === "models") {
157
543
  return [
158
544
  "Run the pi-review model catalog step.",
159
- piHostRules(),
545
+ piHostRules("models"),
160
546
  localeNote,
161
547
  "Execute: pi-review models",
162
548
  "Summarize briefly: provider count and 2–3 model IDs that fit code vs frontend vs plan/challenge (see pi-review skill references/model-selection.md).",
163
- "Do not start a review until the user supplies a target (@path or text).",
549
+ "Do not start a review until the user supplies a natural-language target via /rv or /rv-loop.",
164
550
  ].join("\n\n");
165
551
  }
166
552
 
@@ -169,11 +555,14 @@ export function buildRvOrchestrationPrompt(parsed: RvParsed, locale: RvLocale =
169
555
 
170
556
  if (!target && !parsed.continueHandle) {
171
557
  return [
172
- "/rv needs a review target.",
173
- "Usage: /rv [--mode code|plan|challenge] [--model provider/model] [--keep-session] @files-or-brief",
174
- "Continue: /rv --continue <handle> [--mode ...] [--model ...] [follow-up text]",
175
- "Examples: /rv @src/foo.ts | /rv --mode challenge @docs/design.md | /rv models",
176
- ].join("\n\n");
558
+ parsed.strategy === "loop" ? "/rv-loop needs a review target." : "/rv needs a review target.",
559
+ "Usage:",
560
+ " /rv [--mode code|plan|challenge] [--model provider/model] [--keep-session] <natural-language target>",
561
+ " /rv-loop [--mode ...] [--model ...] [--max-rounds n] <natural-language target>",
562
+ " /rv-models",
563
+ " /rv --continue <handle> [--mode ...] [--model ...] [follow-up text]",
564
+ "Examples: /rv @src | /rv review the auth changes | /rv-loop fix until clean @src | /rv-models",
565
+ ].join("\n");
177
566
  }
178
567
 
179
568
  const modeBlock: Record<string, string> = {
@@ -194,10 +583,14 @@ export function buildRvOrchestrationPrompt(parsed: RvParsed, locale: RvLocale =
194
583
  ].join(" "),
195
584
  };
196
585
 
197
- const cliLine = buildPiReviewArgv(parsed, target).join(" ");
586
+ const cliLine = formatPiReviewCommandLine(parsed, target);
587
+
588
+ const resolutionStep = resolutionNotes.length
589
+ ? `Resolved user shortcuts against the live model catalog:\n${resolutionNotes.map((note) => `- ${note}`).join("\n")}`
590
+ : "";
198
591
 
199
592
  const modelStep = parsed.model
200
- ? `Use model exactly as given: ${parsed.model}${parsed.thinking ? ` at thinking ${parsed.thinking}` : ""} (model must appear in pi-review models output).`
593
+ ? `Use model exactly as resolved: ${parsed.model}${parsed.thinking ? ` at thinking ${parsed.thinking}` : ""}. Prefer this exact catalog id; do not invent a different provider/model.`
201
594
  : parsed.thinking
202
595
  ? `Run pi-review models first if needed; pick using skill references/model-selection.md (code / frontend / plan) or omit --model for Pi default. Use thinking ${parsed.thinking}.`
203
596
  : "Run pi-review models first if needed; pick using skill references/model-selection.md (code / frontend / plan) or omit --model for Pi default.";
@@ -208,24 +601,69 @@ export function buildRvOrchestrationPrompt(parsed: RvParsed, locale: RvLocale =
208
601
  ? "Use --keep-session so the user can follow up with /rv --continue <handle>."
209
602
  : "";
210
603
 
604
+ const singleReviewer = parsed.strategy === "panel" && parsed.reviewers === 1 && !parsed.panel;
605
+ const strategyNote =
606
+ parsed.strategy === "loop"
607
+ ? parsed.until === "clean"
608
+ ? [
609
+ "Strategy: loop until clean (host-driven).",
610
+ `Clean goal: ${RV_CLEAN_GOAL.summary}.`,
611
+ `Hard budget: --max-rounds ${parsed.maxRounds ?? RV_LOOP_UNTIL_CLEAN_DEFAULT_MAX_ROUNDS} (never unlimited).`,
612
+ RV_CLEAN_GOAL.hostProtocol,
613
+ "CLI child remains review-only; you own every fix and re-invocation.",
614
+ ].join("\n")
615
+ : `Strategy: loop closeout. Follow the Loop closeout protocol. /rv-loop defaults to --max-rounds ${RV_LOOP_DEFAULT_MAX_ROUNDS} (host fix point after each gate); explicit value here: ${parsed.maxRounds ?? RV_LOOP_DEFAULT_MAX_ROUNDS}. Note: bare CLI "pi-review loop" still defaults to 3.`
616
+ : singleReviewer
617
+ ? "Strategy: explicit single-reviewer, non-panel review via shell CLI. Do not call pi_review and do not substitute code-experts."
618
+ : "Strategy: panel review via pi_review (code-experts by default unless reviewers/panel are set).";
619
+
620
+ // Shell path for loop/continue/keep-session/no-stream. Native tool path for ordinary /rv panels.
621
+ // Always preserve panel width, per-reviewer models, and consensus fields.
622
+ const untilCleanHostLoop =
623
+ parsed.strategy === "loop" && parsed.until === "clean"
624
+ ? [
625
+ "Host until-clean cycle (required):",
626
+ "A) Execute the shell CLI command below once (one process / one or more internal review rounds).",
627
+ "B) Read the ASCII footer Stop/Exit and status.",
628
+ "C) If Stop=clean and Exit=0 → done. Report clean goal met.",
629
+ "D) If has_findings / exit 1 → fix accepted in-scope blockers only, run proof, then re-execute the same command (count as next host cycle).",
630
+ "E) If needs_human/blocked → stop and escalate.",
631
+ `F) Stop after ${parsed.maxRounds ?? RV_LOOP_UNTIL_CLEAN_DEFAULT_MAX_ROUNDS} host cycles even if not clean; never infinite-loop on an unchanged tree.`,
632
+ ].join("\n")
633
+ : "";
634
+
635
+ const execution =
636
+ parsed.strategy === "loop" || parsed.continueHandle || parsed.keepSession || parsed.noStream || singleReviewer
637
+ ? `Execute exactly (already shell-quoted; do not re-quote or drop flags):\n${cliLine}`
638
+ : buildPiReviewToolCallInstruction(parsed, target);
639
+
211
640
  return [
212
- parsed.continueHandle ? "Continue a prior pi-review session." : "Run pi-review for the target below.",
213
- piHostRules(),
641
+ parsed.continueHandle
642
+ ? "Continue a prior pi-review session."
643
+ : parsed.strategy === "loop"
644
+ ? parsed.until === "clean"
645
+ ? "Run a pi-review until-clean closeout for the target below."
646
+ : "Run a pi-review loop closeout for the target below."
647
+ : singleReviewer
648
+ ? "Run a single-reviewer non-panel pi-review for the target below."
649
+ : "Run pi-review for the target below.",
650
+ piHostRules(parsed.strategy),
214
651
  localeNote,
652
+ strategyNote,
653
+ untilCleanHostLoop,
215
654
  modeBlock[parsed.mode] ??
216
655
  `Mode: ${parsed.mode}. Follow the preset named in review-presets.json when present.`,
656
+ resolutionStep,
217
657
  modelStep,
218
658
  continueNote,
219
- parsed.continueHandle || parsed.keepSession || parsed.noStream
220
- ? `Execute:\n${cliLine}`
221
- : `Call pi_review with target=${JSON.stringify(target)}, mode=${JSON.stringify(parsed.mode)}, and any selected model/thinking values. The tool uses the code-experts panel by default.`,
659
+ execution,
222
660
  ]
223
661
  .filter(Boolean)
224
662
  .join("\n\n");
225
663
  }
226
664
 
227
665
  export const RV_COMPLETIONS: { value: string; hint: string }[] = [
228
- { value: "@", hint: "file path after @ (e.g. @src/foo.ts)" },
666
+ { value: "@", hint: "path mention after @ (e.g. @src or @src/foo.ts); passed as natural-language target text" },
229
667
  { value: "models", hint: "list pi-review models only" },
230
668
  { value: "--mode code", hint: "code / diff / MR review" },
231
669
  { value: "--mode plan", hint: "architecture or plan review" },
@@ -235,4 +673,12 @@ export const RV_COMPLETIONS: { value: string; hint: string }[] = [
235
673
  { value: "--keep-session", hint: "persist session for follow-up" },
236
674
  { value: "--no-stream", hint: "rare: buffer until exit (not default in Pi)" },
237
675
  { value: "--continue ", hint: "resume session; same optional flags as initial /rv" },
676
+ { value: "--max-rounds ", hint: "loop hard budget / review gates (not reviewer count)" },
677
+ { value: "--until clean", hint: "loop goal: host re-review until clean gate (hard-capped by --max-rounds)" },
678
+ { value: "--reviewers ", hint: "panel width: independent reviewers per gate (1-8)" },
679
+ { value: "--panel ", hint: "named panel preset (e.g. code-experts)" },
680
+ { value: "--reviewer-model ", hint: "per-reviewer model: r1=provider/model (repeatable)" },
681
+ { value: "--consensus ", hint: "any|quorum|majority|unanimous" },
682
+ { value: "--min-agree ", hint: "quorum threshold (quorum only)" },
683
+ { value: "--concurrency ", hint: "bound parallel reviewers" },
238
684
  ];