@zephyrdeng/pi-review 0.6.0 → 0.6.2

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.
@@ -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);
@@ -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 tokens = raw.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
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
- mode,
91
- model,
92
- thinking,
93
- keepSession,
94
- noStream,
95
- continueHandle,
96
- modelsOnly,
97
- target: rest.join(" ").trim(),
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 review vs plan/challenge.",
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 a suitable listed ID or omit --model for Pi default. Use thinking ${parsed.thinking}.`
187
- : "Run pi-review models first if needed; pick a suitable listed ID or omit --model for Pi default.";
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.0",
3
+ "version": "0.6.2",
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)** (next to this `SKILL.md`) for Codex/Claude Code defaults: `--progress-log` + background run + tail.
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 provider/model text as `[search]` and verify the exact ID from the output.
89
- - If the user did not name a model, run `pi-review models` and either choose an exact suitable ID from the output or deliberately omit `--model` to use Pi's default.
90
- Completion criterion: the model choice is either an exact listed `provider/model[:thinking]` or an explicit decision to use Pi's default.
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