@zephyrdeng/pi-review 0.6.0 → 0.6.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.
- package/README.md +6 -3
- package/extensions/review.ts +35 -1
- package/extensions/rv-completions.test.ts +26 -29
- package/extensions/rv-completions.ts +150 -111
- package/extensions/rv-locale.test.ts +17 -0
- package/extensions/rv-locale.ts +81 -0
- package/extensions/rv-model-priorities.test.ts +69 -0
- package/extensions/rv-model-priorities.ts +138 -0
- package/extensions/rv-prompts.test.ts +9 -0
- package/extensions/rv-prompts.ts +32 -15
- package/extensions/rv-semantic.test.ts +23 -0
- package/extensions/rv-semantic.ts +110 -0
- package/package.json +3 -3
- package/resources/rv-model-priorities.json +17 -0
- package/skills/pi-review/SKILL.md +9 -6
- package/skills/pi-review/references/codex-tools.md +2 -0
- package/skills/pi-review/references/model-selection.md +72 -0
package/README.md
CHANGED
|
@@ -224,9 +224,12 @@ The `/rv` command injects a task message for the parent agent (Pi host rules: de
|
|
|
224
224
|
|
|
225
225
|
Argument completions are context-aware. After the host session starts, `/rv` reads the live model registry and offers:
|
|
226
226
|
|
|
227
|
-
- **Model list** after `--model
|
|
228
|
-
- **Thinking suffix** after `provider/model:` — only levels the chosen model actually supports
|
|
229
|
-
-
|
|
227
|
+
- **Model list** after `--model `: candidates come from the live Pi registry; order follows **`resources/rv-model-priorities.json`** (override with `PI_REVIEW_RV_PRIORITIES`). Presets match registry ids by substring and prefer newer version strings (e.g. kimi `2.7`, `claude-opus-4-8`). Profiles: **code** (e.g. gpt-5.5 `:xhigh`, glm-5.2 `:high`), **frontend** (vue/css/… → kimi, claude-sonnet, minimax-m3), **plan/challenge** (claude-opus-4-8, deepseek-v4-pro, …).
|
|
228
|
+
- **Thinking suffix** after `provider/model:` — only levels the chosen model actually supports.
|
|
229
|
+
- **Semantic phrases** (e.g. `code review` / 代码审核, `查看模型列表`) in addition to flags; orchestration prompts follow **session locale** (zh/en) for summaries.
|
|
230
|
+
- **Scene templates** at the top level (code / frontend / plan presets).
|
|
231
|
+
|
|
232
|
+
**Claude Code / Codex:** the bundled skill includes **[skills/pi-review/references/model-selection.md](skills/pi-review/references/model-selection.md)** — same presets as `/rv` for choosing `--model` after `pi-review models`.
|
|
230
233
|
|
|
231
234
|
Completions are a hint layer only; execution remains skill-driven. When the model registry is unavailable (e.g. non-TUI mode), `/rv` falls back to the static hint list.
|
|
232
235
|
|
package/extensions/review.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
buildRvCompletions,
|
|
4
4
|
type ModelInfo,
|
|
5
5
|
} from "./rv-completions.js";
|
|
6
|
+
import { detectRvLocale } from "./rv-locale.js";
|
|
6
7
|
import {
|
|
7
8
|
buildRvOrchestrationPrompt,
|
|
8
9
|
parseRvArgs,
|
|
@@ -18,6 +19,28 @@ import {
|
|
|
18
19
|
*/
|
|
19
20
|
let capturedModels: ModelInfo[] | undefined;
|
|
20
21
|
let capturedPrimaryProvider: string | undefined;
|
|
22
|
+
let capturedLocale: ReturnType<typeof detectRvLocale> = "en";
|
|
23
|
+
|
|
24
|
+
function sampleSessionText(
|
|
25
|
+
sessionManager: { getEntries?: () => Array<{ type?: string; content?: unknown }> },
|
|
26
|
+
): string[] {
|
|
27
|
+
const out: string[] = [];
|
|
28
|
+
try {
|
|
29
|
+
const entries = sessionManager.getEntries?.() ?? [];
|
|
30
|
+
for (let i = entries.length - 1; i >= 0 && out.length < 12; i--) {
|
|
31
|
+
const e = entries[i];
|
|
32
|
+
if (e.type !== "message") continue;
|
|
33
|
+
const c = e.content;
|
|
34
|
+
if (typeof c === "string") out.push(c);
|
|
35
|
+
else if (c && typeof c === "object" && "text" in c && typeof (c as { text: string }).text === "string") {
|
|
36
|
+
out.push((c as { text: string }).text);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
/* ignore */
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
21
44
|
|
|
22
45
|
function toModelInfo(m: {
|
|
23
46
|
provider: string;
|
|
@@ -50,12 +73,22 @@ export default function piReviewExtension(pi: ExtensionAPI) {
|
|
|
50
73
|
const available = ctx.modelRegistry?.getAvailable?.() ?? [];
|
|
51
74
|
capturedModels = available.map((m) => toModelInfo(m as unknown as Parameters<typeof toModelInfo>[0]));
|
|
52
75
|
capturedPrimaryProvider = ctx.model?.provider;
|
|
76
|
+
capturedLocale = detectRvLocale(sampleSessionText(ctx.sessionManager));
|
|
53
77
|
} catch {
|
|
54
78
|
capturedModels = undefined;
|
|
55
79
|
capturedPrimaryProvider = undefined;
|
|
80
|
+
capturedLocale = "en";
|
|
56
81
|
}
|
|
57
82
|
});
|
|
58
83
|
|
|
84
|
+
function localeForHandler(ctx: { sessionManager?: Parameters<typeof sampleSessionText>[0] }): typeof capturedLocale {
|
|
85
|
+
try {
|
|
86
|
+
return detectRvLocale(sampleSessionText(ctx.sessionManager ?? { getEntries: () => [] }));
|
|
87
|
+
} catch {
|
|
88
|
+
return capturedLocale;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
59
92
|
pi.registerCommand("rv", {
|
|
60
93
|
description:
|
|
61
94
|
"Delegate pi-review to the agent. Usage: /rv [--mode plan|challenge] [--model id] [--thinking level] [--keep-session] [--no-stream] @target | /rv --continue <handle> [opts] [text] | models",
|
|
@@ -65,6 +98,7 @@ export default function piReviewExtension(pi: ExtensionAPI) {
|
|
|
65
98
|
const dynamic = buildRvCompletions(prefix, {
|
|
66
99
|
models: capturedModels,
|
|
67
100
|
primaryProvider: capturedPrimaryProvider,
|
|
101
|
+
locale: capturedLocale,
|
|
68
102
|
});
|
|
69
103
|
if (dynamic && dynamic.length) return dynamic;
|
|
70
104
|
}
|
|
@@ -93,7 +127,7 @@ export default function piReviewExtension(pi: ExtensionAPI) {
|
|
|
93
127
|
return;
|
|
94
128
|
}
|
|
95
129
|
|
|
96
|
-
pi.sendUserMessage(buildRvOrchestrationPrompt(parsed));
|
|
130
|
+
pi.sendUserMessage(buildRvOrchestrationPrompt(parsed, localeForHandler(ctx)));
|
|
97
131
|
},
|
|
98
132
|
});
|
|
99
133
|
}
|
|
@@ -6,10 +6,11 @@ import {
|
|
|
6
6
|
fuzzyMatch,
|
|
7
7
|
MODE_HINTS,
|
|
8
8
|
rankModelsForReview,
|
|
9
|
-
scoreModelForReview,
|
|
10
9
|
tokenizeArgPrefix,
|
|
11
10
|
type ModelInfo,
|
|
12
11
|
} from "./rv-completions.js";
|
|
12
|
+
import { rvUi } from "./rv-locale.js";
|
|
13
|
+
import { DEFAULT_REVIEW_MODEL_PRIORITIES } from "./rv-model-priorities.js";
|
|
13
14
|
|
|
14
15
|
function model(partial: Partial<ModelInfo> & Pick<ModelInfo, "provider" | "id">): ModelInfo {
|
|
15
16
|
return {
|
|
@@ -72,28 +73,23 @@ describe("extractSignals", () => {
|
|
|
72
73
|
assert.equal(sig.targetExt, "ts");
|
|
73
74
|
});
|
|
74
75
|
|
|
75
|
-
it("carries primary provider
|
|
76
|
+
it("carries primary provider on signals", () => {
|
|
76
77
|
const sig = extractSignals([], { primaryProvider: "anthropic" });
|
|
77
78
|
assert.equal(sig.primaryProvider, "anthropic");
|
|
78
79
|
});
|
|
79
80
|
});
|
|
80
81
|
|
|
81
|
-
describe("
|
|
82
|
-
it("
|
|
83
|
-
const sig =
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
const flash = scoreModelForReview(MODELS[2], sig);
|
|
87
|
-
assert.ok(gpt.score > opus.score, `gpt (${gpt.score}) should beat opus (${opus.score}) for challenge+cross-vendor`);
|
|
88
|
-
assert.ok(opus.score > flash.score, "opus should beat the non-reasoning flash");
|
|
89
|
-
assert.ok(flash.score >= 2, "flash still gets context-window points");
|
|
82
|
+
describe("rankModelsForReview", () => {
|
|
83
|
+
it("puts preset code models first when registered", () => {
|
|
84
|
+
const sig = extractSignals(["@src/a.ts"], {});
|
|
85
|
+
const ranked = rankModelsForReview(MODELS, sig, DEFAULT_REVIEW_MODEL_PRIORITIES);
|
|
86
|
+
assert.equal(ranked[0].id, "gpt-5.5");
|
|
90
87
|
});
|
|
91
88
|
|
|
92
|
-
it("
|
|
93
|
-
const sig =
|
|
94
|
-
const ranked = rankModelsForReview(MODELS, sig);
|
|
95
|
-
assert.equal(ranked[0].id, "
|
|
96
|
-
assert.equal(ranked[ranked.length - 1].id, "gemini-3-flash");
|
|
89
|
+
it("puts preset plan models first for plan mode", () => {
|
|
90
|
+
const sig = extractSignals(["--mode", "plan"], {});
|
|
91
|
+
const ranked = rankModelsForReview(MODELS, sig, DEFAULT_REVIEW_MODEL_PRIORITIES);
|
|
92
|
+
assert.equal(ranked[0].id, "claude-opus-4-6");
|
|
97
93
|
});
|
|
98
94
|
});
|
|
99
95
|
|
|
@@ -106,13 +102,17 @@ describe("fuzzyMatch", () => {
|
|
|
106
102
|
});
|
|
107
103
|
|
|
108
104
|
describe("buildRvCompletions", () => {
|
|
109
|
-
const deps = {
|
|
105
|
+
const deps = {
|
|
106
|
+
models: MODELS,
|
|
107
|
+
primaryProvider: "anthropic",
|
|
108
|
+
priorities: DEFAULT_REVIEW_MODEL_PRIORITIES,
|
|
109
|
+
};
|
|
110
110
|
|
|
111
111
|
it("returns dynamic model list after --model, with recommended marker on top items", () => {
|
|
112
112
|
const items = buildRvCompletions("@x.md --model ", deps);
|
|
113
113
|
assert.ok(items, "expected items");
|
|
114
114
|
assert.ok(items!.some((i) => i.label === "openai/gpt-5.5"));
|
|
115
|
-
const top = items!.find((i) => i.description?.includes("
|
|
115
|
+
const top = items!.find((i) => i.description?.includes("Suggested"));
|
|
116
116
|
assert.ok(top, "expected at least one recommended item");
|
|
117
117
|
// value carries the head so applyCompletion replaces the whole arg safely
|
|
118
118
|
assert.ok(top!.value.startsWith("@x.md --model "));
|
|
@@ -169,11 +169,11 @@ describe("buildRvCompletions", () => {
|
|
|
169
169
|
|
|
170
170
|
it("offers scene templates + models keyword at empty top level", () => {
|
|
171
171
|
const items = buildRvCompletions("", deps);
|
|
172
|
+
const ui = rvUi("en");
|
|
172
173
|
assert.ok(items);
|
|
173
|
-
assert.ok(items!.some((i) => i.label ===
|
|
174
|
-
assert.ok(items!.some((i) => i.label.
|
|
175
|
-
|
|
176
|
-
const tmpl = items!.find((i) => i.label.startsWith("审代码改动"))!;
|
|
174
|
+
assert.ok(items!.some((i) => i.label === ui.listModels));
|
|
175
|
+
assert.ok(items!.some((i) => i.label === ui.codePreset));
|
|
176
|
+
const tmpl = items!.find((i) => i.label === ui.codePreset)!;
|
|
177
177
|
assert.ok(tmpl.value.endsWith(" @"));
|
|
178
178
|
});
|
|
179
179
|
|
|
@@ -202,15 +202,12 @@ describe("buildRvCompletions", () => {
|
|
|
202
202
|
}
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
-
it("scene templates
|
|
205
|
+
it("omits preset scene templates when registry has no preset id match", () => {
|
|
206
206
|
const nonReasoning = [{ ...MODELS[2], thinkingLevels: [] }] as ModelInfo[];
|
|
207
|
-
const items = buildRvCompletions("", { models: nonReasoning });
|
|
207
|
+
const items = buildRvCompletions("", { models: nonReasoning, priorities: DEFAULT_REVIEW_MODEL_PRIORITIES });
|
|
208
208
|
assert.ok(items);
|
|
209
|
-
|
|
210
|
-
assert.ok(
|
|
211
|
-
// Must NOT contain a dangling colon like `gemini-3-flash: @`
|
|
212
|
-
assert.doesNotMatch(tmpl!.value, /flash:\s@/);
|
|
213
|
-
assert.match(tmpl!.value, /flash @/);
|
|
209
|
+
assert.ok(!items!.some((i) => i.label === rvUi("en").codePreset));
|
|
210
|
+
assert.ok(items!.some((i) => i.label === rvUi("en").listModels));
|
|
214
211
|
});
|
|
215
212
|
|
|
216
213
|
it("falls back gracefully when models are unavailable (static flags still work)", () => {
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure, testable /rv argument-completion logic.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* across hosts. The thin runtime adapter (registry capture + AutocompleteItem
|
|
7
|
-
* shaping) lives in `review.ts`.
|
|
8
|
-
*
|
|
9
|
-
* Completion items are returned "续写式": `value` always carries the already-typed
|
|
10
|
-
* head tokens because pi-tui's `applyCompletion` replaces the *entire* argument
|
|
11
|
-
* text after the slash command with `item.value`.
|
|
4
|
+
* Avoids pi types so tokenizer + preset ordering can be unit-tested.
|
|
5
|
+
* Completion `value` must include the typed head (pi-tui replaces the whole arg).
|
|
12
6
|
*/
|
|
13
7
|
|
|
8
|
+
import { type RvLocale, rvUi } from "./rv-locale.js";
|
|
9
|
+
import { semanticCompletionItems, SEMANTIC_PHRASES } from "./rv-semantic.js";
|
|
10
|
+
import {
|
|
11
|
+
loadReviewModelPriorities,
|
|
12
|
+
primaryPresetForProfile,
|
|
13
|
+
rankModelsWithPresets,
|
|
14
|
+
resolvePresetOrderedModels,
|
|
15
|
+
resolveReviewProfile,
|
|
16
|
+
type ReviewModelPriorities,
|
|
17
|
+
type ReviewProfile,
|
|
18
|
+
} from "./rv-model-priorities.js";
|
|
19
|
+
|
|
14
20
|
export type AutocompleteItem = {
|
|
15
21
|
value: string;
|
|
16
22
|
label: string;
|
|
@@ -36,18 +42,18 @@ export const RV_FLAGS = [
|
|
|
36
42
|
] as const;
|
|
37
43
|
|
|
38
44
|
export const MODE_HINTS: Record<string, string> = {
|
|
39
|
-
code: "
|
|
40
|
-
plan: "
|
|
41
|
-
challenge: "
|
|
45
|
+
code: "Code / diff / MR review",
|
|
46
|
+
plan: "Architecture or plan review",
|
|
47
|
+
challenge: "Adversarial plan review",
|
|
42
48
|
};
|
|
43
49
|
|
|
44
50
|
export const FLAG_HINTS: Record<string, string> = {
|
|
45
|
-
"--mode": "
|
|
51
|
+
"--mode": "Review mode: code | plan | challenge",
|
|
46
52
|
"--model": "provider/model[:thinking]",
|
|
47
|
-
"--thinking": "off|minimal|low|medium|high|xhigh",
|
|
48
|
-
"--keep-session": "
|
|
49
|
-
"--no-stream": "
|
|
50
|
-
"--continue": "
|
|
53
|
+
"--thinking": "off | minimal | low | medium | high | xhigh",
|
|
54
|
+
"--keep-session": "Keep session for /rv --continue follow-up",
|
|
55
|
+
"--no-stream": "Buffer output until exit (not default)",
|
|
56
|
+
"--continue": "Resume an existing review session",
|
|
51
57
|
};
|
|
52
58
|
|
|
53
59
|
/** Minimal model metadata we need; converted from the host's Model registry. */
|
|
@@ -74,6 +80,31 @@ export interface ReviewSignals {
|
|
|
74
80
|
export interface CompletionDeps {
|
|
75
81
|
models?: ModelInfo[];
|
|
76
82
|
primaryProvider?: string;
|
|
83
|
+
priorities?: ReviewModelPriorities;
|
|
84
|
+
locale?: RvLocale;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function localeOf(deps: CompletionDeps): RvLocale {
|
|
88
|
+
return deps.locale ?? "en";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function modeFromSemanticHead(head: string[]): string | undefined {
|
|
92
|
+
const joined = head.join(" ").toLowerCase();
|
|
93
|
+
for (const row of SEMANTIC_PHRASES) {
|
|
94
|
+
if (!row.apply.mode) continue;
|
|
95
|
+
for (const p of row.phrases) {
|
|
96
|
+
if (joined.includes(p.toLowerCase())) return row.apply.mode;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getPriorities(deps: CompletionDeps): ReviewModelPriorities {
|
|
103
|
+
return deps.priorities ?? loadReviewModelPriorities();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function reviewProfileForSignals(sig: ReviewSignals): ReviewProfile {
|
|
107
|
+
return resolveReviewProfile(sig.mode, sig.targetExt);
|
|
77
108
|
}
|
|
78
109
|
|
|
79
110
|
// ---------------------------------------------------------------------------
|
|
@@ -111,12 +142,6 @@ export function tokenizeArgPrefix(prefix: string): TokenizedArgs {
|
|
|
111
142
|
// Signal extraction
|
|
112
143
|
// ---------------------------------------------------------------------------
|
|
113
144
|
|
|
114
|
-
const CODE_EXTS = new Set([
|
|
115
|
-
"ts", "tsx", "js", "jsx", "mjs", "cjs", "py", "rb", "go", "rs", "java", "kt",
|
|
116
|
-
"c", "cc", "cpp", "h", "hpp", "cs", "php", "swift", "scala", "sh", "sql",
|
|
117
|
-
"vue", "svelte",
|
|
118
|
-
]);
|
|
119
|
-
const PLAN_EXTS = new Set(["md", "markdown", "txt", "rst", "adoc"]);
|
|
120
145
|
|
|
121
146
|
function extOf(token: string): string | undefined {
|
|
122
147
|
const cleaned = token.replace(/^['"]|['"]$/g, "").replace(/^@/, "");
|
|
@@ -127,7 +152,8 @@ function extOf(token: string): string | undefined {
|
|
|
127
152
|
export function extractSignals(head: string[], deps: CompletionDeps): ReviewSignals {
|
|
128
153
|
const target = head.find((t) => t.startsWith("@"));
|
|
129
154
|
const modeIdx = head.indexOf("--mode");
|
|
130
|
-
const mode =
|
|
155
|
+
const mode =
|
|
156
|
+
(modeIdx >= 0 ? head[modeIdx + 1] : undefined) ?? modeFromSemanticHead(head) ?? "code";
|
|
131
157
|
return {
|
|
132
158
|
mode,
|
|
133
159
|
hasTarget: Boolean(target),
|
|
@@ -137,46 +163,27 @@ export function extractSignals(head: string[], deps: CompletionDeps): ReviewSign
|
|
|
137
163
|
}
|
|
138
164
|
|
|
139
165
|
// ---------------------------------------------------------------------------
|
|
140
|
-
// Model
|
|
166
|
+
// Model ordering: registry + preset priorities (no LLM)
|
|
141
167
|
// ---------------------------------------------------------------------------
|
|
142
168
|
|
|
143
|
-
export function scoreModelForReview(
|
|
144
|
-
m: ModelInfo,
|
|
145
|
-
sig: ReviewSignals,
|
|
146
|
-
): { score: number; reason: string } {
|
|
147
|
-
let score = 0;
|
|
148
|
-
const reasons: string[] = [];
|
|
149
|
-
if (m.reasoning) {
|
|
150
|
-
score += 3;
|
|
151
|
-
reasons.push("reasoning");
|
|
152
|
-
}
|
|
153
|
-
if (m.contextWindow >= 200_000) score += 2;
|
|
154
|
-
else if (m.contextWindow >= 100_000) score += 1;
|
|
155
|
-
if (sig.primaryProvider && m.provider !== sig.primaryProvider) {
|
|
156
|
-
score += 1;
|
|
157
|
-
reasons.push("跨厂商交叉审核");
|
|
158
|
-
}
|
|
159
|
-
if ((sig.mode === "plan" || sig.mode === "challenge") && m.thinkingLevels.includes("xhigh")) {
|
|
160
|
-
score += 2;
|
|
161
|
-
reasons.push("xhigh 思考");
|
|
162
|
-
} else if (m.thinkingLevels.includes("high")) {
|
|
163
|
-
score += 1;
|
|
164
|
-
}
|
|
165
|
-
if (sig.targetExt && PLAN_EXTS.has(sig.targetExt) && m.reasoning) {
|
|
166
|
-
score += 1;
|
|
167
|
-
reasons.push("适合文档评审");
|
|
168
|
-
}
|
|
169
|
-
return { score, reason: reasons.filter(Boolean).join(" · ") };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
169
|
export function rankModelsForReview(
|
|
173
170
|
models: ModelInfo[],
|
|
174
171
|
sig: ReviewSignals,
|
|
172
|
+
priorities?: ReviewModelPriorities,
|
|
175
173
|
): ModelInfo[] {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
174
|
+
const profile = reviewProfileForSignals(sig);
|
|
175
|
+
return rankModelsWithPresets(models, profile, priorities ?? loadReviewModelPriorities());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function presetDescription(
|
|
179
|
+
presetLabel: string,
|
|
180
|
+
profile: ReviewProfile,
|
|
181
|
+
presetRank: number | undefined,
|
|
182
|
+
locale: RvLocale,
|
|
183
|
+
): string {
|
|
184
|
+
if (presetRank === undefined) return "";
|
|
185
|
+
const ui = rvUi(locale);
|
|
186
|
+
return `${ui.presetTier(presetRank)} · ${profile} · ${presetLabel}`;
|
|
180
187
|
}
|
|
181
188
|
|
|
182
189
|
// ---------------------------------------------------------------------------
|
|
@@ -234,16 +241,16 @@ function wrap(
|
|
|
234
241
|
}));
|
|
235
242
|
}
|
|
236
243
|
|
|
237
|
-
function thinkingDescription(level: string, sig: ReviewSignals): string {
|
|
244
|
+
function thinkingDescription(level: string, sig: ReviewSignals, locale: RvLocale): string {
|
|
238
245
|
const recommended = sig.mode === "code" ? "high" : "xhigh";
|
|
239
|
-
const star = level === recommended ?
|
|
246
|
+
const star = level === recommended ? rvUi(locale).thinkingSuggested : "";
|
|
240
247
|
const map: Record<string, string> = {
|
|
241
|
-
off: "
|
|
242
|
-
minimal: "
|
|
243
|
-
low: "
|
|
244
|
-
medium: "
|
|
245
|
-
high: "
|
|
246
|
-
xhigh: "
|
|
248
|
+
off: "No thinking (fastest)",
|
|
249
|
+
minimal: "Minimal thinking",
|
|
250
|
+
low: "Low effort",
|
|
251
|
+
medium: "Medium effort",
|
|
252
|
+
high: "High effort (common for review)",
|
|
253
|
+
xhigh: "Max effort (plan/challenge)",
|
|
247
254
|
};
|
|
248
255
|
return `${star}${map[level] ?? level}`;
|
|
249
256
|
}
|
|
@@ -259,22 +266,25 @@ function modelItems(
|
|
|
259
266
|
sig: ReviewSignals,
|
|
260
267
|
headPrefix: string,
|
|
261
268
|
tail: string,
|
|
269
|
+
deps: CompletionDeps,
|
|
262
270
|
): AutocompleteItem[] | null {
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
271
|
+
const priorities = getPriorities(deps);
|
|
272
|
+
const profile = reviewProfileForSignals(sig);
|
|
273
|
+
const presetRows = resolvePresetOrderedModels(models, profile, priorities);
|
|
274
|
+
const presetByLabel = new Map(presetRows.map((p) => [p.model.label, p]));
|
|
275
|
+
const ranked = rankModelsWithPresets(models, profile, priorities);
|
|
276
|
+
const items = ranked.map((m) => {
|
|
277
|
+
const preset = presetByLabel.get(m.label);
|
|
278
|
+
const tag = profile;
|
|
269
279
|
return {
|
|
270
280
|
value: `${headPrefix}${m.label}`,
|
|
271
281
|
label: m.label,
|
|
272
282
|
description: [
|
|
273
|
-
|
|
283
|
+
preset ? presetDescription(preset.presetLabel, profile, preset.presetRank, localeOf(deps)) : "",
|
|
274
284
|
tag,
|
|
275
285
|
m.reasoning ? "reasoning" : "",
|
|
276
286
|
contextLabel(m.contextWindow),
|
|
277
|
-
|
|
287
|
+
preset?.thinking ? `preset thinking ${preset.thinking}` : "",
|
|
278
288
|
]
|
|
279
289
|
.filter(Boolean)
|
|
280
290
|
.join(" · "),
|
|
@@ -301,6 +311,7 @@ function thinkingSuffixItems(
|
|
|
301
311
|
models: ModelInfo[],
|
|
302
312
|
headPrefix: string,
|
|
303
313
|
sig: ReviewSignals,
|
|
314
|
+
deps: CompletionDeps,
|
|
304
315
|
): AutocompleteItem[] | null {
|
|
305
316
|
const colonIdx = tail.lastIndexOf(":");
|
|
306
317
|
const modelPart = tail.slice(0, colonIdx);
|
|
@@ -319,40 +330,64 @@ function thinkingSuffixItems(
|
|
|
319
330
|
return filtered.map((level) => ({
|
|
320
331
|
value: `${headPrefix}${modelPart}:${level}`,
|
|
321
332
|
label: level,
|
|
322
|
-
description: thinkingDescription(level, sig),
|
|
333
|
+
description: thinkingDescription(level, sig, localeOf(deps)),
|
|
323
334
|
}));
|
|
324
335
|
}
|
|
325
336
|
|
|
337
|
+
function thinkingSuffixForModel(m: ModelInfo, want?: string): string {
|
|
338
|
+
const lvl =
|
|
339
|
+
want && m.thinkingLevels.includes(want)
|
|
340
|
+
? want
|
|
341
|
+
: want && m.thinkingLevels.includes("high")
|
|
342
|
+
? "high"
|
|
343
|
+
: m.thinkingLevels.includes("xhigh")
|
|
344
|
+
? "xhigh"
|
|
345
|
+
: m.thinkingLevels.includes("high")
|
|
346
|
+
? "high"
|
|
347
|
+
: (m.thinkingLevels[0] ?? "");
|
|
348
|
+
return lvl ? `:${lvl}` : "";
|
|
349
|
+
}
|
|
350
|
+
|
|
326
351
|
function sceneTemplates(
|
|
327
|
-
|
|
352
|
+
models: ModelInfo[],
|
|
353
|
+
sig: ReviewSignals,
|
|
328
354
|
headPrefix: string,
|
|
329
355
|
tail: string,
|
|
356
|
+
deps: CompletionDeps,
|
|
330
357
|
): AutocompleteItem[] {
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
358
|
+
const priorities = getPriorities(deps);
|
|
359
|
+
const codePrimary = primaryPresetForProfile(models, "code", priorities);
|
|
360
|
+
const planPrimary = primaryPresetForProfile(models, "plan", priorities);
|
|
361
|
+
const frontendPrimary = primaryPresetForProfile(models, "frontend", priorities);
|
|
362
|
+
|
|
363
|
+
const ui = rvUi(localeOf(deps));
|
|
364
|
+
const templates: { cmd: string; label: string }[] = [];
|
|
365
|
+
if (codePrimary) {
|
|
366
|
+
const m = codePrimary.model;
|
|
367
|
+
templates.push({
|
|
368
|
+
cmd: `${ui.modeCode} --model ${m.label}${thinkingSuffixForModel(m, codePrimary.thinking ?? "xhigh")} @`,
|
|
369
|
+
label: ui.codePreset,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
if (frontendPrimary) {
|
|
373
|
+
const m = frontendPrimary.model;
|
|
374
|
+
templates.push({
|
|
375
|
+
cmd: `${ui.modeCode} --model ${m.label}${thinkingSuffixForModel(m, frontendPrimary.thinking)} @`,
|
|
376
|
+
label: ui.frontendPreset,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (planPrimary) {
|
|
380
|
+
const m = planPrimary.model;
|
|
381
|
+
templates.push({
|
|
382
|
+
cmd: `${ui.modePlan} --model ${m.label}${thinkingSuffixForModel(m, planPrimary.thinking ?? "high")} @`,
|
|
383
|
+
label: ui.planPreset,
|
|
384
|
+
});
|
|
385
|
+
templates.push({
|
|
386
|
+
cmd: `${ui.modeChallenge} ${ui.keepSession} --model ${m.label}${thinkingSuffixForModel(m, "xhigh")} @`,
|
|
387
|
+
label: ui.challengePreset,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
341
390
|
|
|
342
|
-
const templates = [
|
|
343
|
-
{
|
|
344
|
-
cmd: `--mode code --model ${top.label}${suffix("high")} @`,
|
|
345
|
-
label: "审代码改动(推荐配置)",
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
cmd: `--mode plan --model ${top.label}${suffix("high")} @`,
|
|
349
|
-
label: "架构 / 计划评审",
|
|
350
|
-
},
|
|
351
|
-
{
|
|
352
|
-
cmd: `--mode challenge --keep-session --model ${top.label}${suffix("xhigh")} @`,
|
|
353
|
-
label: "对抗性方案评审(可追问)",
|
|
354
|
-
},
|
|
355
|
-
];
|
|
356
391
|
return templates
|
|
357
392
|
.filter((t) => fuzzyMatch(t.label, tail) || t.cmd.includes(tail))
|
|
358
393
|
.map((t) => ({
|
|
@@ -382,21 +417,25 @@ function topLevelCompletions(
|
|
|
382
417
|
const items: AutocompleteItem[] = [];
|
|
383
418
|
|
|
384
419
|
// Scene templates (only at top level, before any target/mode chosen is fine).
|
|
385
|
-
|
|
386
|
-
|
|
420
|
+
if (deps.models?.length) {
|
|
421
|
+
items.push(...sceneTemplates(deps.models, sig, headPrefix, tail, deps));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
items.push(...semanticCompletionItems(localeOf(deps), tail, headPrefix, head));
|
|
387
425
|
|
|
388
|
-
|
|
389
|
-
if (fuzzyMatch(
|
|
426
|
+
const ui = rvUi(localeOf(deps));
|
|
427
|
+
if (fuzzyMatch(ui.modelsKeyword, tail) || ui.modelsKeyword.startsWith(tail) || fuzzyMatch("models", tail)) {
|
|
390
428
|
items.push({
|
|
391
|
-
value: `${headPrefix}
|
|
392
|
-
label:
|
|
393
|
-
description:
|
|
429
|
+
value: `${headPrefix}${ui.listModels}`,
|
|
430
|
+
label: ui.listModels,
|
|
431
|
+
description: ui.listModelsDesc,
|
|
394
432
|
});
|
|
395
433
|
}
|
|
396
434
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
435
|
+
if (tail.startsWith("--")) {
|
|
436
|
+
const flags = flagItems(head, tail, headPrefix);
|
|
437
|
+
if (flags) items.push(...flags);
|
|
438
|
+
}
|
|
400
439
|
|
|
401
440
|
return items.length ? items : null;
|
|
402
441
|
}
|
|
@@ -418,10 +457,10 @@ export function buildRvCompletions(
|
|
|
418
457
|
if (prev === "--model") {
|
|
419
458
|
const colonIdx = tail.lastIndexOf(":");
|
|
420
459
|
if (colonIdx !== -1 && tail.slice(0, colonIdx).includes("/")) {
|
|
421
|
-
const items = thinkingSuffixItems(tail, deps.models ?? [], headPrefix, sig);
|
|
460
|
+
const items = thinkingSuffixItems(tail, deps.models ?? [], headPrefix, sig, deps);
|
|
422
461
|
return items;
|
|
423
462
|
}
|
|
424
|
-
return modelItems(deps.models ?? [], sig, headPrefix, tail);
|
|
463
|
+
return modelItems(deps.models ?? [], sig, headPrefix, tail, deps);
|
|
425
464
|
}
|
|
426
465
|
|
|
427
466
|
// 2) --mode value position.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { detectRvLocale } from "./rv-locale.js";
|
|
4
|
+
|
|
5
|
+
describe("detectRvLocale", () => {
|
|
6
|
+
it("returns zh when recent messages are mostly Chinese", () => {
|
|
7
|
+
assert.equal(detectRvLocale(["帮我看一下这个方案", "好的"]), "zh");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("returns en for English-only samples", () => {
|
|
11
|
+
assert.equal(detectRvLocale(["Please review this diff", "Thanks"]), "en");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("defaults to en when empty", () => {
|
|
15
|
+
assert.equal(detectRvLocale([]), "en");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Lightweight locale for /rv UI strings (no LLM). */
|
|
2
|
+
|
|
3
|
+
export type RvLocale = "en" | "zh";
|
|
4
|
+
|
|
5
|
+
const CJK_RE = /[\u4e00-\u9fff\u3400-\u4dbf]/;
|
|
6
|
+
|
|
7
|
+
export function detectRvLocale(textSamples: string[]): RvLocale {
|
|
8
|
+
let cjk = 0;
|
|
9
|
+
let latin = 0;
|
|
10
|
+
for (const sample of textSamples) {
|
|
11
|
+
const t = sample.slice(0, 2000);
|
|
12
|
+
for (const ch of t) {
|
|
13
|
+
if (CJK_RE.test(ch)) cjk++;
|
|
14
|
+
else if (/[a-zA-Z]/.test(ch)) latin++;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (cjk === 0 && latin === 0) return "en";
|
|
18
|
+
return cjk >= latin * 0.15 ? "zh" : "en";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type RvUiStrings = {
|
|
22
|
+
modelsKeyword: string;
|
|
23
|
+
modelsDesc: string;
|
|
24
|
+
modeCode: string;
|
|
25
|
+
modePlan: string;
|
|
26
|
+
modeChallenge: string;
|
|
27
|
+
keepSession: string;
|
|
28
|
+
keepSessionDesc: string;
|
|
29
|
+
listModels: string;
|
|
30
|
+
listModelsDesc: string;
|
|
31
|
+
codePreset: string;
|
|
32
|
+
frontendPreset: string;
|
|
33
|
+
planPreset: string;
|
|
34
|
+
challengePreset: string;
|
|
35
|
+
suggested: string;
|
|
36
|
+
presetTier: (n: number) => string;
|
|
37
|
+
thinkingSuggested: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const UI: Record<RvLocale, RvUiStrings> = {
|
|
41
|
+
en: {
|
|
42
|
+
modelsKeyword: "models",
|
|
43
|
+
modelsDesc: "List pi-review models only",
|
|
44
|
+
modeCode: "code review",
|
|
45
|
+
modePlan: "plan review",
|
|
46
|
+
modeChallenge: "challenge review",
|
|
47
|
+
keepSession: "keep session for follow-up",
|
|
48
|
+
keepSessionDesc: "Persist review session for /rv --continue",
|
|
49
|
+
listModels: "list models",
|
|
50
|
+
listModelsDesc: "Show provider/model catalog",
|
|
51
|
+
codePreset: "Code review (preset)",
|
|
52
|
+
frontendPreset: "Frontend / multimodal review (preset)",
|
|
53
|
+
planPreset: "Plan review (preset)",
|
|
54
|
+
challengePreset: "Challenge review (preset)",
|
|
55
|
+
suggested: "Suggested",
|
|
56
|
+
presetTier: (n) => (n === 0 ? "Suggested" : `Preset #${n + 1}`),
|
|
57
|
+
thinkingSuggested: "Suggested ",
|
|
58
|
+
},
|
|
59
|
+
zh: {
|
|
60
|
+
modelsKeyword: "models",
|
|
61
|
+
modelsDesc: "仅列出 pi-review 可用模型",
|
|
62
|
+
modeCode: "代码审核",
|
|
63
|
+
modePlan: "方案审核",
|
|
64
|
+
modeChallenge: "对抗性审核",
|
|
65
|
+
keepSession: "保留会话可追问",
|
|
66
|
+
keepSessionDesc: "保留 review 会话,便于 /rv --continue",
|
|
67
|
+
listModels: "查看模型列表",
|
|
68
|
+
listModelsDesc: "列出当前可用 provider/model",
|
|
69
|
+
codePreset: "代码审核(推荐配置)",
|
|
70
|
+
frontendPreset: "前端 / 多模态审核(推荐)",
|
|
71
|
+
planPreset: "方案 / 架构审核(推荐)",
|
|
72
|
+
challengePreset: "对抗性方案审核(可追问)",
|
|
73
|
+
suggested: "推荐",
|
|
74
|
+
presetTier: (n) => (n === 0 ? "推荐" : `预设 #${n + 1}`),
|
|
75
|
+
thinkingSuggested: "推荐 ",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export function rvUi(locale: RvLocale): RvUiStrings {
|
|
80
|
+
return UI[locale];
|
|
81
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_REVIEW_MODEL_PRIORITIES,
|
|
5
|
+
matchPresetEntry,
|
|
6
|
+
rankModelsWithPresets,
|
|
7
|
+
resolveReviewProfile,
|
|
8
|
+
type PresetModelEntry,
|
|
9
|
+
} from "./rv-model-priorities.js";
|
|
10
|
+
import type { ModelInfo } from "./rv-completions.js";
|
|
11
|
+
|
|
12
|
+
function m(provider: string, id: string, extra?: Partial<ModelInfo>): ModelInfo {
|
|
13
|
+
return {
|
|
14
|
+
provider,
|
|
15
|
+
id,
|
|
16
|
+
label: `${provider}/${id}`,
|
|
17
|
+
name: id,
|
|
18
|
+
reasoning: true,
|
|
19
|
+
contextWindow: 200_000,
|
|
20
|
+
thinkingLevels: ["high", "xhigh"],
|
|
21
|
+
...extra,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("resolveReviewProfile", () => {
|
|
26
|
+
it("uses plan profile for plan/challenge mode", () => {
|
|
27
|
+
assert.equal(resolveReviewProfile("plan", "ts"), "plan");
|
|
28
|
+
assert.equal(resolveReviewProfile("challenge", "md"), "plan");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("uses frontend profile for vue/svelte/css targets", () => {
|
|
32
|
+
assert.equal(resolveReviewProfile("code", "vue"), "frontend");
|
|
33
|
+
assert.equal(resolveReviewProfile("code", "css"), "frontend");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("defaults to code profile", () => {
|
|
37
|
+
assert.equal(resolveReviewProfile("code", "ts"), "code");
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("matchPresetEntry", () => {
|
|
42
|
+
const registry = [
|
|
43
|
+
m("openai", "gpt-5.5"),
|
|
44
|
+
m("zhipu", "glm-5.2-chat"),
|
|
45
|
+
m("moonshot", "kimi-k2.7-preview"),
|
|
46
|
+
m("moonshot", "kimi-k2.5"),
|
|
47
|
+
m("anthropic", "claude-opus-4-8"),
|
|
48
|
+
m("anthropic", "claude-opus-4-6"),
|
|
49
|
+
m("deepseek", "deepseek-v4-pro"),
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
it("picks newest kimi when versionPrefer 2.7", () => {
|
|
53
|
+
const entry: PresetModelEntry = { idContains: "kimi", versionPrefer: "2.7" };
|
|
54
|
+
const hit = matchPresetEntry(registry, entry);
|
|
55
|
+
assert.ok(hit);
|
|
56
|
+
assert.match(hit!.id, /2\.7|k2\.7/i);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("ranks gpt-5.5 first for code presets", () => {
|
|
60
|
+
const ordered = rankModelsWithPresets(registry, "code", DEFAULT_REVIEW_MODEL_PRIORITIES);
|
|
61
|
+
assert.equal(ordered[0].id, "gpt-5.5");
|
|
62
|
+
assert.equal(ordered[1].id, "glm-5.2-chat");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("ranks claude-opus-4-8 before older opus for plan", () => {
|
|
66
|
+
const ordered = rankModelsWithPresets(registry, "plan", DEFAULT_REVIEW_MODEL_PRIORITIES);
|
|
67
|
+
assert.equal(ordered[0].id, "claude-opus-4-8");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { ModelInfo } from "./rv-completions.js";
|
|
5
|
+
|
|
6
|
+
export type PresetModelEntry = {
|
|
7
|
+
idContains: string;
|
|
8
|
+
thinking?: string;
|
|
9
|
+
versionPrefer?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ReviewModelPriorities = {
|
|
13
|
+
code: PresetModelEntry[];
|
|
14
|
+
frontend: PresetModelEntry[];
|
|
15
|
+
plan: PresetModelEntry[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_REVIEW_MODEL_PRIORITIES: ReviewModelPriorities = {
|
|
19
|
+
code: [
|
|
20
|
+
{ idContains: "gpt-5.5", thinking: "xhigh" },
|
|
21
|
+
{ idContains: "glm-5.2", thinking: "high" },
|
|
22
|
+
],
|
|
23
|
+
frontend: [
|
|
24
|
+
{ idContains: "kimi", versionPrefer: "2.7" },
|
|
25
|
+
{ idContains: "claude-sonnet", versionPrefer: "5" },
|
|
26
|
+
{ idContains: "minimax-m3" },
|
|
27
|
+
],
|
|
28
|
+
plan: [
|
|
29
|
+
{ idContains: "claude-opus-4-8" },
|
|
30
|
+
{ idContains: "claude-opus-4", versionPrefer: "8" },
|
|
31
|
+
{ idContains: "deepseek-v4-pro" },
|
|
32
|
+
{ idContains: "deepseek-v4" },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const DEFAULT_PRIORITIES_PATH = path.resolve(__dirname, "../resources/rv-model-priorities.json");
|
|
38
|
+
|
|
39
|
+
export function loadReviewModelPriorities(
|
|
40
|
+
filePath: string = process.env.PI_REVIEW_RV_PRIORITIES || DEFAULT_PRIORITIES_PATH,
|
|
41
|
+
): ReviewModelPriorities {
|
|
42
|
+
try {
|
|
43
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
44
|
+
const parsed = JSON.parse(raw) as Partial<ReviewModelPriorities>;
|
|
45
|
+
return {
|
|
46
|
+
code: parsed.code?.length ? parsed.code : DEFAULT_REVIEW_MODEL_PRIORITIES.code,
|
|
47
|
+
frontend: parsed.frontend?.length ? parsed.frontend : DEFAULT_REVIEW_MODEL_PRIORITIES.frontend,
|
|
48
|
+
plan: parsed.plan?.length ? parsed.plan : DEFAULT_REVIEW_MODEL_PRIORITIES.plan,
|
|
49
|
+
};
|
|
50
|
+
} catch {
|
|
51
|
+
return DEFAULT_REVIEW_MODEL_PRIORITIES;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type ReviewProfile = "code" | "frontend" | "plan";
|
|
56
|
+
|
|
57
|
+
const FRONTEND_EXTS = new Set([
|
|
58
|
+
"vue", "svelte", "css", "scss", "sass", "less", "styl", "html", "htm",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
export function resolveReviewProfile(mode: string, targetExt?: string): ReviewProfile {
|
|
62
|
+
if (mode === "plan" || mode === "challenge") return "plan";
|
|
63
|
+
if (targetExt && FRONTEND_EXTS.has(targetExt)) return "frontend";
|
|
64
|
+
return "code";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function idVersionSortKey(id: string): string {
|
|
68
|
+
return id.toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function matchPresetEntry(
|
|
72
|
+
models: ModelInfo[],
|
|
73
|
+
entry: PresetModelEntry,
|
|
74
|
+
): ModelInfo | undefined {
|
|
75
|
+
const needle = entry.idContains.toLowerCase();
|
|
76
|
+
let candidates = models.filter((m) => m.id.toLowerCase().includes(needle));
|
|
77
|
+
if (entry.versionPrefer) {
|
|
78
|
+
const vp = entry.versionPrefer.toLowerCase();
|
|
79
|
+
const withVersion = candidates.filter((m) => m.id.toLowerCase().includes(vp));
|
|
80
|
+
if (withVersion.length) candidates = withVersion;
|
|
81
|
+
}
|
|
82
|
+
if (!candidates.length) return undefined;
|
|
83
|
+
candidates.sort((a, b) =>
|
|
84
|
+
idVersionSortKey(b.id).localeCompare(idVersionSortKey(a.id), undefined, { numeric: true }),
|
|
85
|
+
);
|
|
86
|
+
return candidates[0];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type ResolvedPresetModel = {
|
|
90
|
+
model: ModelInfo;
|
|
91
|
+
thinking?: string;
|
|
92
|
+
presetRank: number;
|
|
93
|
+
presetLabel: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export function resolvePresetOrderedModels(
|
|
97
|
+
models: ModelInfo[],
|
|
98
|
+
profile: ReviewProfile,
|
|
99
|
+
priorities: ReviewModelPriorities,
|
|
100
|
+
): ResolvedPresetModel[] {
|
|
101
|
+
const rows = priorities[profile];
|
|
102
|
+
const seen = new Set<string>();
|
|
103
|
+
const out: ResolvedPresetModel[] = [];
|
|
104
|
+
for (let i = 0; i < rows.length; i++) {
|
|
105
|
+
const entry = rows[i];
|
|
106
|
+
const matched = matchPresetEntry(models, entry);
|
|
107
|
+
if (!matched || seen.has(matched.label)) continue;
|
|
108
|
+
seen.add(matched.label);
|
|
109
|
+
out.push({
|
|
110
|
+
model: matched,
|
|
111
|
+
thinking: entry.thinking,
|
|
112
|
+
presetRank: i,
|
|
113
|
+
presetLabel: entry.idContains,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function rankModelsWithPresets(
|
|
120
|
+
models: ModelInfo[],
|
|
121
|
+
profile: ReviewProfile,
|
|
122
|
+
priorities: ReviewModelPriorities,
|
|
123
|
+
): ModelInfo[] {
|
|
124
|
+
const preset = resolvePresetOrderedModels(models, profile, priorities);
|
|
125
|
+
const presetLabels = new Set(preset.map((p) => p.model.label));
|
|
126
|
+
const rest = models
|
|
127
|
+
.filter((m) => !presetLabels.has(m.label))
|
|
128
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
129
|
+
return [...preset.map((p) => p.model), ...rest];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function primaryPresetForProfile(
|
|
133
|
+
models: ModelInfo[],
|
|
134
|
+
profile: ReviewProfile,
|
|
135
|
+
priorities: ReviewModelPriorities,
|
|
136
|
+
): ResolvedPresetModel | undefined {
|
|
137
|
+
return resolvePresetOrderedModels(models, profile, priorities)[0];
|
|
138
|
+
}
|
|
@@ -3,6 +3,7 @@ import { describe, it } from "node:test";
|
|
|
3
3
|
import {
|
|
4
4
|
buildPiReviewArgv,
|
|
5
5
|
buildRvOrchestrationPrompt,
|
|
6
|
+
orchestrationLocaleNote,
|
|
6
7
|
parseRvArgs,
|
|
7
8
|
validateRvParsed,
|
|
8
9
|
} from "./rv-prompts.js";
|
|
@@ -57,6 +58,14 @@ describe("parseRvArgs / validateRvParsed", () => {
|
|
|
57
58
|
]);
|
|
58
59
|
});
|
|
59
60
|
|
|
61
|
+
it("models orchestration includes locale note and model-selection reference", () => {
|
|
62
|
+
const p = parseRvArgs("models");
|
|
63
|
+
const zh = buildRvOrchestrationPrompt(p, "zh");
|
|
64
|
+
assert.match(zh, /用中文向用户总结/);
|
|
65
|
+
assert.match(zh, /model-selection/);
|
|
66
|
+
assert.match(orchestrationLocaleNote("en"), /English/);
|
|
67
|
+
});
|
|
68
|
+
|
|
60
69
|
it("orchestration prompt forbids default no-stream and progress-log in Pi", () => {
|
|
61
70
|
const p = parseRvArgs("@src/foo.ts");
|
|
62
71
|
const prompt = buildRvOrchestrationPrompt(p);
|
package/extensions/rv-prompts.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/** /rv → parent-agent task text; child review uses resources/review-presets.json */
|
|
2
2
|
|
|
3
|
+
import type { RvLocale } from "./rv-locale.js";
|
|
4
|
+
import { mergeSemanticIntoParsed, stripSemanticPhrases } from "./rv-semantic.js";
|
|
5
|
+
|
|
3
6
|
export const RV_MODES = ["code", "plan", "challenge"] as const;
|
|
4
7
|
export type RvMode = (typeof RV_MODES)[number];
|
|
5
8
|
|
|
@@ -21,7 +24,8 @@ export type RvValidation =
|
|
|
21
24
|
| { ok: false; message: string };
|
|
22
25
|
|
|
23
26
|
export function parseRvArgs(raw: string): RvParsed {
|
|
24
|
-
const
|
|
27
|
+
const { remainder, apply: semanticApply } = stripSemanticPhrases(raw);
|
|
28
|
+
const tokens = remainder.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
|
|
25
29
|
const cleaned = tokens.map((t) => t.replace(/^['"]|['"]$/g, ""));
|
|
26
30
|
|
|
27
31
|
let mode = "code";
|
|
@@ -86,16 +90,19 @@ export function parseRvArgs(raw: string): RvParsed {
|
|
|
86
90
|
rest.push(t);
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
93
|
+
return mergeSemanticIntoParsed(
|
|
94
|
+
{
|
|
95
|
+
mode,
|
|
96
|
+
model,
|
|
97
|
+
thinking,
|
|
98
|
+
keepSession,
|
|
99
|
+
noStream,
|
|
100
|
+
continueHandle,
|
|
101
|
+
modelsOnly,
|
|
102
|
+
target: rest.join(" ").trim(),
|
|
103
|
+
},
|
|
104
|
+
semanticApply,
|
|
105
|
+
);
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
export function validateRvParsed(parsed: RvParsed): RvValidation {
|
|
@@ -126,6 +133,13 @@ export function buildPiReviewArgv(parsed: RvParsed, target: string): string[] {
|
|
|
126
133
|
return parts;
|
|
127
134
|
}
|
|
128
135
|
|
|
136
|
+
export function orchestrationLocaleNote(locale: RvLocale = "en"): string {
|
|
137
|
+
if (locale === "zh") {
|
|
138
|
+
return "用中文向用户总结(模型目录、选型理由、review 结论);技术 id 如 provider/model 可保留英文。";
|
|
139
|
+
}
|
|
140
|
+
return "Summarize for the user in English (catalog, model rationale, review conclusion); keep technical ids such as provider/model as listed.";
|
|
141
|
+
}
|
|
142
|
+
|
|
129
143
|
function piHostRules(): string {
|
|
130
144
|
return [
|
|
131
145
|
"Follow the pi-review skill.",
|
|
@@ -137,13 +151,15 @@ function piHostRules(): string {
|
|
|
137
151
|
].join(" ");
|
|
138
152
|
}
|
|
139
153
|
|
|
140
|
-
export function buildRvOrchestrationPrompt(parsed: RvParsed): string {
|
|
154
|
+
export function buildRvOrchestrationPrompt(parsed: RvParsed, locale: RvLocale = "en"): string {
|
|
155
|
+
const localeNote = orchestrationLocaleNote(locale);
|
|
141
156
|
if (parsed.modelsOnly) {
|
|
142
157
|
return [
|
|
143
158
|
"Run the pi-review model catalog step.",
|
|
144
159
|
piHostRules(),
|
|
160
|
+
localeNote,
|
|
145
161
|
"Execute: pi-review models",
|
|
146
|
-
"Summarize briefly: provider count and 2–3 model IDs that fit code
|
|
162
|
+
"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).",
|
|
147
163
|
"Do not start a review until the user supplies a target (@path or text).",
|
|
148
164
|
].join("\n\n");
|
|
149
165
|
}
|
|
@@ -183,8 +199,8 @@ export function buildRvOrchestrationPrompt(parsed: RvParsed): string {
|
|
|
183
199
|
const modelStep = parsed.model
|
|
184
200
|
? `Use model exactly as given: ${parsed.model}${parsed.thinking ? ` at thinking ${parsed.thinking}` : ""} (model must appear in pi-review models output).`
|
|
185
201
|
: parsed.thinking
|
|
186
|
-
? `Run pi-review models first if needed; pick
|
|
187
|
-
: "Run pi-review models first if needed; pick
|
|
202
|
+
? `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
|
+
: "Run pi-review models first if needed; pick using skill references/model-selection.md (code / frontend / plan) or omit --model for Pi default.";
|
|
188
204
|
|
|
189
205
|
const continueNote = parsed.continueHandle
|
|
190
206
|
? "Continuing an existing review session. Optional --mode and --model match the same flags as an initial /rv run."
|
|
@@ -195,6 +211,7 @@ export function buildRvOrchestrationPrompt(parsed: RvParsed): string {
|
|
|
195
211
|
return [
|
|
196
212
|
parsed.continueHandle ? "Continue a prior pi-review session." : "Run pi-review for the target below.",
|
|
197
213
|
piHostRules(),
|
|
214
|
+
localeNote,
|
|
198
215
|
modeBlock[parsed.mode] ??
|
|
199
216
|
`Mode: ${parsed.mode}. Follow the preset named in review-presets.json when present.`,
|
|
200
217
|
modelStep,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { parseRvArgs } from "./rv-prompts.js";
|
|
4
|
+
import { stripSemanticPhrases } from "./rv-semantic.js";
|
|
5
|
+
|
|
6
|
+
describe("stripSemanticPhrases / parseRvArgs", () => {
|
|
7
|
+
it("maps 方案审核 to plan mode", () => {
|
|
8
|
+
const p = parseRvArgs("方案审核 @docs/plan.md");
|
|
9
|
+
assert.equal(p.mode, "plan");
|
|
10
|
+
assert.equal(p.target, "@docs/plan.md");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("maps list models phrase to modelsOnly", () => {
|
|
14
|
+
const p = parseRvArgs("查看模型列表");
|
|
15
|
+
assert.equal(p.modelsOnly, true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("strips code review before flag parse", () => {
|
|
19
|
+
const { remainder, apply } = stripSemanticPhrases("code review @src/a.ts");
|
|
20
|
+
assert.equal(apply.mode, "code");
|
|
21
|
+
assert.match(remainder, /@src\/a\.ts/);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic /rv phrases (localized labels, not raw CLI flags in the input box).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { RvParsed } from "./rv-prompts.js";
|
|
6
|
+
import type { RvLocale } from "./rv-locale.js";
|
|
7
|
+
import { rvUi } from "./rv-locale.js";
|
|
8
|
+
|
|
9
|
+
export type SemanticApply = {
|
|
10
|
+
mode?: string;
|
|
11
|
+
keepSession?: boolean;
|
|
12
|
+
modelsOnly?: boolean;
|
|
13
|
+
noStream?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type SemanticPhrase = {
|
|
17
|
+
phrases: string[];
|
|
18
|
+
apply: SemanticApply;
|
|
19
|
+
completionValue: (locale: RvLocale) => string;
|
|
20
|
+
completionDesc?: (locale: RvLocale) => string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const SEMANTIC_PHRASES: SemanticPhrase[] = [
|
|
24
|
+
{
|
|
25
|
+
phrases: ["code review", "代码审核", "代码审查", "审代码"],
|
|
26
|
+
apply: { mode: "code" },
|
|
27
|
+
completionValue: (l) => rvUi(l).modeCode,
|
|
28
|
+
completionDesc: (l) => (l === "zh" ? "聚焦 diff / 正确性 / 测试" : "Diff, correctness, tests"),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
phrases: ["plan review", "方案审核", "方案评审", "架构审核", "审方案"],
|
|
32
|
+
apply: { mode: "plan" },
|
|
33
|
+
completionValue: (l) => rvUi(l).modePlan,
|
|
34
|
+
completionDesc: (l) => (l === "zh" ? "架构 / 设计 / 产品方案" : "Architecture and design"),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
phrases: ["challenge review", "对抗性审核", "对抗审核", "挑战审核"],
|
|
38
|
+
apply: { mode: "challenge" },
|
|
39
|
+
completionValue: (l) => rvUi(l).modeChallenge,
|
|
40
|
+
completionDesc: (l) => (l === "zh" ? "压力测试假设与风险" : "Adversarial plan pressure-test"),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
phrases: ["keep session for follow-up", "保留会话可追问", "保留会话", "keep session"],
|
|
44
|
+
apply: { keepSession: true },
|
|
45
|
+
completionValue: (l) => rvUi(l).keepSession,
|
|
46
|
+
completionDesc: (l) => rvUi(l).keepSessionDesc,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
phrases: ["list models", "查看模型列表", "模型列表", "list available models"],
|
|
50
|
+
apply: { modelsOnly: true },
|
|
51
|
+
completionValue: (l) => rvUi(l).listModels,
|
|
52
|
+
completionDesc: (l) => rvUi(l).listModelsDesc,
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
export function stripSemanticPhrases(raw: string): { remainder: string; apply: SemanticApply } {
|
|
57
|
+
let remainder = raw;
|
|
58
|
+
const merged: SemanticApply = {};
|
|
59
|
+
const sorted = [...SEMANTIC_PHRASES].flatMap((row) =>
|
|
60
|
+
row.phrases.map((p) => ({ phrase: p, apply: row.apply })),
|
|
61
|
+
);
|
|
62
|
+
sorted.sort((a, b) => b.phrase.length - a.phrase.length);
|
|
63
|
+
|
|
64
|
+
for (const { phrase, apply } of sorted) {
|
|
65
|
+
const re = new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
66
|
+
if (!re.test(remainder)) continue;
|
|
67
|
+
remainder = remainder.replace(re, " ").replace(/\s+/g, " ").trim();
|
|
68
|
+
if (apply.mode) merged.mode = apply.mode;
|
|
69
|
+
if (apply.keepSession) merged.keepSession = true;
|
|
70
|
+
if (apply.modelsOnly) merged.modelsOnly = true;
|
|
71
|
+
if (apply.noStream) merged.noStream = true;
|
|
72
|
+
}
|
|
73
|
+
return { remainder, apply: merged };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function mergeSemanticIntoParsed(parsed: RvParsed, apply: SemanticApply): RvParsed {
|
|
77
|
+
const out = { ...parsed };
|
|
78
|
+
if (apply.mode) out.mode = apply.mode;
|
|
79
|
+
if (apply.keepSession) out.keepSession = true;
|
|
80
|
+
if (apply.modelsOnly) out.modelsOnly = true;
|
|
81
|
+
if (apply.noStream) out.noStream = true;
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function semanticCompletionItems(
|
|
86
|
+
locale: RvLocale,
|
|
87
|
+
tail: string,
|
|
88
|
+
headPrefix: string,
|
|
89
|
+
head: string[],
|
|
90
|
+
): { value: string; label: string; description?: string }[] {
|
|
91
|
+
const present = new Set(head.map((h) => h.toLowerCase()));
|
|
92
|
+
const q = tail.toLowerCase().trim();
|
|
93
|
+
const items: { value: string; label: string; description?: string }[] = [];
|
|
94
|
+
for (const row of SEMANTIC_PHRASES) {
|
|
95
|
+
const value = row.completionValue(locale);
|
|
96
|
+
if (present.has(value.toLowerCase())) continue;
|
|
97
|
+
const matches =
|
|
98
|
+
!q ||
|
|
99
|
+
value.toLowerCase().includes(q) ||
|
|
100
|
+
value.toLowerCase().startsWith(q) ||
|
|
101
|
+
row.phrases.some((p) => p.toLowerCase().startsWith(q) || p.toLowerCase().includes(q));
|
|
102
|
+
if (!matches) continue;
|
|
103
|
+
items.push({
|
|
104
|
+
value: `${headPrefix}${value}`,
|
|
105
|
+
label: value,
|
|
106
|
+
description: row.completionDesc?.(locale),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return items;
|
|
110
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zephyrdeng/pi-review",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
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>",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"build": "tsc",
|
|
68
68
|
"check": "tsc --noEmit",
|
|
69
69
|
"smoke": "node dist/cli.js --help >/dev/null",
|
|
70
|
-
"test": "npx --yes tsx --test src/child-process.test.ts src/review.meta.test.ts src/meta-footer.test.ts src/json-events.test.ts src/install-args.test.ts extensions/rv-prompts.test.ts extensions/rv-completions.test.ts",
|
|
71
|
-
"test:rv": "npx --yes tsx --test extensions/rv-prompts.test.ts extensions/rv-completions.test.ts",
|
|
70
|
+
"test": "npx --yes tsx --test src/child-process.test.ts src/review.meta.test.ts src/meta-footer.test.ts src/json-events.test.ts src/install-args.test.ts extensions/rv-prompts.test.ts extensions/rv-completions.test.ts extensions/rv-model-priorities.test.ts extensions/rv-locale.test.ts extensions/rv-semantic.test.ts",
|
|
71
|
+
"test:rv": "npx --yes tsx --test extensions/rv-prompts.test.ts extensions/rv-completions.test.ts extensions/rv-model-priorities.test.ts extensions/rv-locale.test.ts extensions/rv-semantic.test.ts",
|
|
72
72
|
"prepublishOnly": "npm run build",
|
|
73
73
|
"pack:dry": "npm pack --dry-run"
|
|
74
74
|
},
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"code": [
|
|
3
|
+
{ "idContains": "gpt-5.5", "thinking": "xhigh" },
|
|
4
|
+
{ "idContains": "glm-5.2", "thinking": "high" }
|
|
5
|
+
],
|
|
6
|
+
"frontend": [
|
|
7
|
+
{ "idContains": "kimi", "versionPrefer": "2.7" },
|
|
8
|
+
{ "idContains": "claude-sonnet", "versionPrefer": "5" },
|
|
9
|
+
{ "idContains": "minimax-m3" }
|
|
10
|
+
],
|
|
11
|
+
"plan": [
|
|
12
|
+
{ "idContains": "claude-opus-4-8" },
|
|
13
|
+
{ "idContains": "claude-opus-4", "versionPrefer": "8" },
|
|
14
|
+
{ "idContains": "deepseek-v4-pro" },
|
|
15
|
+
{ "idContains": "deepseek-v4" }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pi-review
|
|
3
|
-
description: Use pi-review to delegate isolated code, diff, repository, architecture, or plan reviews to a fresh Pi session and return only the review conclusion. On Claude Code or Codex, default to --progress-log with a background shell run and tail the log (chat tools buffer stdout). Use when the user asks for Pi review, isolated review, code review, plan review, challenge review, review status/progress, or wants to avoid context pollution.
|
|
3
|
+
description: Use pi-review to delegate isolated code, diff, repository, architecture, or plan reviews to a fresh Pi session and return only the review conclusion. Model selection presets for code, frontend, and plan review (see references/model-selection.md). On Claude Code or Codex, default to --progress-log with a background shell run and tail the log (chat tools buffer stdout). Use when the user asks for Pi review, isolated review, code review, plan review, challenge review, review status/progress, or wants to avoid context pollution.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Pi Review
|
|
@@ -26,7 +26,8 @@ When resolving this fallback, use the actual directory that contains this `SKILL
|
|
|
26
26
|
## Codex / Claude Code
|
|
27
27
|
|
|
28
28
|
- Map skill mentions of `Bash` to your host shell tool.
|
|
29
|
-
- Read **[references/codex-tools.md](./references/codex-tools.md)**
|
|
29
|
+
- Read **[references/codex-tools.md](./references/codex-tools.md)** for `--progress-log` + background run + tail.
|
|
30
|
+
- Read **[references/model-selection.md](./references/model-selection.md)** when choosing `--model` after `pi-review models` (code / frontend / plan presets).
|
|
30
31
|
- Do not use a blocking foreground shell alone for live progress; follow **Default workflow by host** below.
|
|
31
32
|
|
|
32
33
|
## Default workflow by host
|
|
@@ -79,15 +80,17 @@ Do not implement findings from a status check; status is informational only.
|
|
|
79
80
|
|
|
80
81
|
## Steps
|
|
81
82
|
|
|
82
|
-
1. Prime the model catalog:
|
|
83
|
+
1. Prime the model catalog and pick a model:
|
|
83
84
|
```bash
|
|
84
85
|
pi-review models [search]
|
|
85
86
|
```
|
|
86
87
|
Rules:
|
|
87
88
|
- Run this before building the review command.
|
|
88
|
-
- If the user named a model, use that
|
|
89
|
-
- If the user did not name a model,
|
|
90
|
-
|
|
89
|
+
- If the user named a model, use that text as `[search]` and verify the **exact** listed `provider/model` from the output.
|
|
90
|
+
- If the user did not name a model, follow **[references/model-selection.md](./references/model-selection.md)**: infer profile (code / frontend / plan), match the priority list against catalog ids, then set `--model` with optional `:thinking` when supported.
|
|
91
|
+
- On **Claude Code / Codex / Cursor**, state the chosen model in one line (match the user's language) before running the review command.
|
|
92
|
+
- If nothing in the priority list matches, omit `--model` (Pi default) or pick a listed reasoning model with large context—say which and why.
|
|
93
|
+
Completion criterion: exact listed `provider/model[:thinking]` or an explicit default; never invented ids.
|
|
91
94
|
|
|
92
95
|
2. Choose one mode:
|
|
93
96
|
- default `code` — code, diff, MR, file, or repository review.
|
|
@@ -25,6 +25,8 @@ When the **pi-review** skill applies on Codex, Claude Code, or Cursor:
|
|
|
25
25
|
|
|
26
26
|
See the parent skill: sections *Default workflow by host* and *Streaming vs agent hosts (Claude Code / Codex)*.
|
|
27
27
|
|
|
28
|
+
**Model choice:** after `pi-review models`, use **[model-selection.md](./model-selection.md)** (code / frontend / plan presets)—same guidance as Pi `/rv` completions.
|
|
29
|
+
|
|
28
30
|
Example tail (milestone events only):
|
|
29
31
|
|
|
30
32
|
```bash
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Model selection for pi-review (all hosts)
|
|
2
|
+
|
|
3
|
+
Use this after `pi-review models` (or `pi-review models <search>`). **Never invent** `provider/model` IDs—only IDs that appear in that output.
|
|
4
|
+
|
|
5
|
+
## Workflow (Claude Code, Codex, Cursor, Pi parent agent)
|
|
6
|
+
|
|
7
|
+
1. Run `pi-review models` (narrow with search if the user named a vendor or model family).
|
|
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`).
|
|
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
|
+
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.
|
|
12
|
+
|
|
13
|
+
## Profiles
|
|
14
|
+
|
|
15
|
+
| Profile | When to use |
|
|
16
|
+
|---------|-------------|
|
|
17
|
+
| **code** | Default: diffs, MRs, backend/ general code paths (`@src/...`, `.ts`, `.go`, …). |
|
|
18
|
+
| **frontend** | UI-heavy targets: `.vue`, `.svelte`, `.css`, `.scss`, `.html`, or user says frontend / UI / multimodal. |
|
|
19
|
+
| **plan** | `--mode plan` or `--mode challenge`, or reviewing `.md` / design / architecture docs (not line-by-line code only). |
|
|
20
|
+
|
|
21
|
+
## Priority lists (same as Pi `/rv` package preset)
|
|
22
|
+
|
|
23
|
+
Configurable on disk for Pi: `resources/rv-model-priorities.json` or `PI_REVIEW_RV_PRIORITIES`. Agents should follow this table unless the user overrides.
|
|
24
|
+
|
|
25
|
+
### Code review
|
|
26
|
+
|
|
27
|
+
| Order | Match model id containing | Suggested thinking |
|
|
28
|
+
|-------|---------------------------|------------------|
|
|
29
|
+
| 1 | `gpt-5.5` | `xhigh` |
|
|
30
|
+
| 2 | `glm-5.2` | `high` |
|
|
31
|
+
|
|
32
|
+
### Frontend / multimodal review
|
|
33
|
+
|
|
34
|
+
| Order | Match model id containing | Notes |
|
|
35
|
+
|-------|---------------------------|--------|
|
|
36
|
+
| 1 | `kimi` | Prefer id that also contains `2.7` when multiple Kimi ids exist |
|
|
37
|
+
| 2 | `claude-sonnet` | Prefer id containing `5` when multiple |
|
|
38
|
+
| 3 | `minimax-m3` | Multimodal-friendly |
|
|
39
|
+
|
|
40
|
+
Use **code** mode (`pi-review` default) unless the user asked for plan/challenge.
|
|
41
|
+
|
|
42
|
+
### Plan / challenge review
|
|
43
|
+
|
|
44
|
+
| Order | Match model id containing |
|
|
45
|
+
|-------|---------------------------|
|
|
46
|
+
| 1 | `claude-opus-4-8` |
|
|
47
|
+
| 2 | `claude-opus-4` (+ prefer `8` in id if several) |
|
|
48
|
+
| 3 | `deepseek-v4-pro` |
|
|
49
|
+
| 4 | `deepseek-v4` |
|
|
50
|
+
|
|
51
|
+
Use `--mode plan` or `--mode challenge` as appropriate.
|
|
52
|
+
|
|
53
|
+
## Examples (illustrative—always substitute exact catalog strings)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Code
|
|
57
|
+
pi-review --model openai-codex/gpt-5.5:xhigh -- @src/auth.ts
|
|
58
|
+
|
|
59
|
+
# Frontend
|
|
60
|
+
pi-review --model wafer.ai/Kimi-K2.6 -- @src/App.vue
|
|
61
|
+
|
|
62
|
+
# Plan
|
|
63
|
+
pi-review --mode plan --model px:anthropic/claude-opus-4-8 -- @docs/architecture.md
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## User-named model
|
|
67
|
+
|
|
68
|
+
If the user specifies a model, run `pi-review models <fragment>` and use the **exact** listed id. Do not normalize or guess provider prefixes.
|
|
69
|
+
|
|
70
|
+
## Language
|
|
71
|
+
|
|
72
|
+
Explain your model choice briefly in the **same language** as the user's request (Chinese ↔ English). The review body language is independent; this rule is only for the one-line rationale before/after invoking `pi-review`.
|