@fyeeme/pi-review 2.0.0 → 2.1.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.
@@ -7,7 +7,8 @@
7
7
  * no host finding-renderer, so this tool does double duty: it renders a tidy
8
8
  * Chinese Markdown report (table + details) back to the conversation AND writes
9
9
  * a machine-readable JSON (findings + level + outcome) to
10
- * `<cwd>/.pi/review/<id>.json` so CI / --fix / --comment can consume it.
10
+ * `<cwd>/<CONFIG_DIR_NAME>/review/<id>.json` so CI / --fix / --comment can
11
+ * consume it.
11
12
  *
12
13
  * `verdict` (CONFIRMED/PLAUSIBLE) and `outcome` (fixed/skipped/no_change_needed)
13
14
  * enums follow the CC ReportFindings shape — values verified against the CC
@@ -16,8 +17,9 @@
16
17
  * normalizes stray invalid values (drop the finding / coerce to skipped) rather
17
18
  * than failing the whole call.
18
19
  */
19
- import { defineTool, getMarkdownTheme } from "@earendil-works/pi-coding-agent";
20
+ import { defineTool, getMarkdownTheme, CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
20
21
  import { Markdown } from "@earendil-works/pi-tui";
22
+ import { StringEnum } from "@earendil-works/pi-ai";
21
23
  import { type Static, Type } from "typebox";
22
24
  import * as fs from "node:fs";
23
25
  import * as path from "node:path";
@@ -27,25 +29,32 @@ import * as path from "node:path";
27
29
  // verdict 两值与 outcome 三档在 2.1.223/226/227 三版本中一致。
28
30
 
29
31
  const VERDICT_VALUES = ["CONFIRMED", "PLAUSIBLE"] as const;
30
- const Verdict = Type.Union(VERDICT_VALUES.map((v) => Type.Literal(v)));
32
+ const Verdict = StringEnum(VERDICT_VALUES);
31
33
 
32
- const OUTCOME_VALUES = ["fixed", "skipped", "no_change_needed"] as const;
33
34
  /** CC ReportFindings `outcome` 三档(2.1.227 二进制实证)。fixed-later 再上报时更新。 */
34
- const Outcome = Type.Union(OUTCOME_VALUES.map((v) => Type.Literal(v)));
35
+ export const OUTCOME_VALUES = ["fixed", "skipped", "no_change_needed"] as const;
36
+ const Outcome = StringEnum(OUTCOME_VALUES);
37
+
38
+ /** --loop 的 blocking 阈值:P0/P1 触发修复→再评审一轮(P2/P3 只入报告)。 */
39
+ const PRIORITY_VALUES = ["P0", "P1", "P2", "P3"] as const;
40
+ const Priority = StringEnum(PRIORITY_VALUES, {
41
+ description:
42
+ "优先级 P0(阻断,立刻修)/ P1(高)/ P2(中)/ P3(低)。--loop 循环修复以 P0/P1 为 blocking 阈值;省略视为 P2。",
43
+ });
35
44
 
36
45
  // 供 SKILL-schema 同步测试引用(防漂移:SKILL 流程契约不得与常量脱节)。
37
- export { OUTCOME_VALUES, VERDICT_VALUES };
46
+ export { PRIORITY_VALUES, VERDICT_VALUES };
38
47
 
39
- const Level = Type.Union([
40
- Type.Literal("low"),
41
- Type.Literal("medium"),
42
- Type.Literal("high"),
43
- Type.Literal("xhigh"),
44
- Type.Literal("max"),
48
+ const Level = StringEnum([
49
+ "low",
50
+ "medium",
51
+ "high",
52
+ "xhigh",
53
+ "max",
45
54
  // simplify reuses this tool for structured apply-outcome reporting
46
55
  // (harden-code-simplify). Not a review effort level — carries no verdict.
47
- Type.Literal("simplify"),
48
- ]);
56
+ "simplify",
57
+ ] as const);
49
58
 
50
59
  // --- schema -----------------------------------------------------------------
51
60
 
@@ -57,6 +66,7 @@ const FindingParams = Type.Object({
57
66
  "产生该发现的角度 slug:correctness / reuse / simplification / efficiency / altitude / conventions(或更具体如 test-coverage)。",
58
67
  }),
59
68
  verdict: Type.Optional(Verdict),
69
+ priority: Type.Optional(Priority),
60
70
  short_summary: Type.Optional(
61
71
  Type.String({
62
72
  description:
@@ -106,6 +116,7 @@ type LooseFinding = {
106
116
  line?: number;
107
117
  category: string;
108
118
  verdict?: string;
119
+ priority?: string;
109
120
  short_summary?: string;
110
121
  summary: string;
111
122
  failure_scenario: string;
@@ -124,7 +135,10 @@ function sanitizeFinding(f: LooseFinding): { f: LooseFinding; note?: string } |
124
135
  note = `(outcome "${outcome}" 非法,已归一化为 skipped)`;
125
136
  outcome = "skipped";
126
137
  }
127
- return { f: { ...f, outcome }, note };
138
+ // 非法 priority 静默丢弃(降至未标注),不影响该条 finding 存活。
139
+ const priority =
140
+ f.priority !== undefined && (PRIORITY_VALUES as readonly string[]).includes(f.priority) ? f.priority : undefined;
141
+ return { f: { ...f, outcome, priority }, note };
128
142
  }
129
143
 
130
144
  /**
@@ -153,6 +167,7 @@ interface FindingInput {
153
167
  line?: number;
154
168
  category: string;
155
169
  verdict?: string;
170
+ priority?: string;
156
171
  short_summary?: string;
157
172
  summary: string;
158
173
  failure_scenario: string;
@@ -200,13 +215,14 @@ function renderReport(p: ReportInput): string {
200
215
  lines.push("|---|------|------|------|------|");
201
216
  for (let i = 0; i < p.findings.length; i++) {
202
217
  const f = p.findings[i]!;
203
- lines.push(`| ${i + 1} | ${escapeCell(f.verdict ?? "")} | ${escapeCell(f.category)} | ${escapeCell(fmtLoc(f))} | ${escapeCell(f.short_summary ?? f.summary)} |`);
218
+ const verdictCell = [f.priority, f.verdict].filter(Boolean).join(" · ");
219
+ lines.push(`| ${i + 1} | ${escapeCell(verdictCell)} | ${escapeCell(f.category)} | ${escapeCell(fmtLoc(f))} | ${escapeCell(f.short_summary ?? f.summary)} |`);
204
220
  }
205
221
  lines.push("");
206
222
  lines.push("**详情**");
207
223
  lines.push("");
208
224
  p.findings.forEach((f, i) => {
209
- const v = f.verdict ? ` *(${f.verdict})*` : "";
225
+ const v = [f.priority, f.verdict].filter(Boolean).length > 0 ? ` *(${[f.priority, f.verdict].filter(Boolean).join(" · ")})*` : "";
210
226
  const out = f.outcome ? `\n修复结果:\`${f.outcome}\`` : "";
211
227
  const note = f.note ? `\n${f.note}` : "";
212
228
  lines.push(`**${i + 1}. ${fmtLoc(f)} — ${f.category}**${v}`);
@@ -223,12 +239,12 @@ export const reviewReportTool = defineTool<typeof ReviewReportParams, ReviewRepo
223
239
  name: "review_report",
224
240
  label: "Report review findings",
225
241
  description:
226
- "Report code-review findings as a typed list — Pi's counterpart to CC's ReportFindings. Use this only when the active code-review instructions tell you to report findings with this tool. Call it once with the verified findings ranked most-severe first (empty array if nothing survived verification) and do not also print the findings as text — the tool renders a tidy Chinese Markdown report back to the conversation AND writes a machine-readable JSON to <cwd>/.pi/review/ for CI / --fix / --comment. When re-reporting after applying fixes, set `outcome` on each finding. 上报结构化 code-review 发现(CC ReportFindings 的 Pi 对等物)。",
242
+ "Report code-review findings as a typed list — Pi's counterpart to CC's ReportFindings. Use this only when the active code-review instructions tell you to report findings with this tool. Call it once with the verified findings ranked most-severe first (empty array if nothing survived verification) and do not also print the findings as text — the tool renders a tidy Chinese Markdown report back to the conversation AND writes a machine-readable JSON to the project's pi config dir (CONFIG_DIR_NAME, typically `.pi`) under review/ for CI / --fix / --comment. When re-reporting after applying fixes, set `outcome` on each finding. 上报结构化 code-review 发现(CC ReportFindings 的 Pi 对等物)。",
227
243
  promptSnippet: "review_report — report structured code-review findings (renders Markdown + writes JSON for CI)",
228
244
  promptGuidelines: [
229
245
  "After verify + dedup, call `review_report` once with { level, findings } (most-severe first; empty array if none survived). Do not also hand-write the Markdown table — this tool renders it.",
230
246
  "On re-report after --fix, set each finding's `outcome` (fixed / skipped / no_change_needed).",
231
- "Use this tool only when the code-review skill instructs reporting findings; otherwise follow the active output format.",
247
+ "Use `review_report` only when the code-review skill instructs reporting findings; otherwise follow the active output format.",
232
248
  ],
233
249
  parameters: ReviewReportParams,
234
250
 
@@ -267,7 +283,7 @@ export const reviewReportTool = defineTool<typeof ReviewReportParams, ReviewRepo
267
283
  let writeError: string | null = null;
268
284
  const now = new Date();
269
285
  try {
270
- const dir = path.join(ctx.cwd, ".pi", "review");
286
+ const dir = path.join(ctx.cwd, CONFIG_DIR_NAME, "review");
271
287
  await fs.promises.mkdir(dir, { recursive: true });
272
288
  const safeId = toolCallId.replace(/[^\w.-]+/g, "_");
273
289
  const ts = now.toISOString().replace(/[:.]/g, "-");
@@ -1,100 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import * as fs from "node:fs";
3
- import * as os from "node:os";
4
- import * as path from "node:path";
5
- import { bundledSkillPath } from "../skills.ts";
6
-
7
- /** Effort levels the /code-review command accepts (mirrors CC's effort enum). */
8
- export const REVIEW_LEVELS = ["low", "medium", "high", "xhigh", "max"] as const;
9
- export type ReviewLevel = (typeof REVIEW_LEVELS)[number];
10
-
11
- const DEFAULT_LEVEL: ReviewLevel = "low";
12
-
13
- /** Where the last explicitly-typed effort is persisted (CC 2.1.223 codeReviewLastEffort). */
14
- const STATE_FILE = path.join(os.homedir(), ".pi", ".pi-review-state.json");
15
-
16
- export type EffortSource = "explicit" | "last-used" | "default";
17
-
18
- /**
19
- * Parse a leading effort level out of raw args; the remainder (flags + target)
20
- * is returned verbatim. Pure — unit-testable. Mirrors CC's ecl()/Tjn(): the
21
- * first token is the level only if it matches the enum; otherwise the whole
22
- * string is the target/flags.
23
- */
24
- export function parseReviewArgs(args: string): { level: ReviewLevel | undefined; rest: string } {
25
- const tokens = (args ?? "").trim().split(/\s+/).filter(Boolean);
26
- if (tokens.length === 0) return { level: undefined, rest: "" };
27
- const first = tokens[0]!.toLowerCase();
28
- const isLevel = (REVIEW_LEVELS as readonly string[]).includes(first);
29
- return {
30
- level: isLevel ? (first as ReviewLevel) : undefined,
31
- rest: isLevel ? tokens.slice(1).join(" ") : tokens.join(" "),
32
- };
33
- }
34
-
35
- /**
36
- * Resolve the effective effort + where it came from. Pure — unit-testable.
37
- * Mirrors CC 2.1.223: explicit wins; otherwise reuse the last-typed level;
38
- * otherwise the default. (220 defaulted straight to low with no memory.)
39
- */
40
- export function resolveEffort(
41
- explicit: ReviewLevel | undefined,
42
- lastUsed: ReviewLevel | undefined,
43
- ): { level: ReviewLevel; source: EffortSource } {
44
- if (explicit) return { level: explicit, source: "explicit" };
45
- if (lastUsed) return { level: lastUsed, source: "last-used" };
46
- return { level: DEFAULT_LEVEL, source: "default" };
47
- }
48
-
49
- // Best-effort persistence — sticky-effort is a convenience, not a correctness
50
- // invariant; a read/write failure must not break the review.
51
- function readLastEffort(): ReviewLevel | undefined {
52
- try {
53
- const raw = JSON.parse(fs.readFileSync(STATE_FILE, "utf8")) as { codeReviewLastEffort?: unknown };
54
- const v = raw.codeReviewLastEffort;
55
- return typeof v === "string" && (REVIEW_LEVELS as readonly string[]).includes(v)
56
- ? (v as ReviewLevel)
57
- : undefined;
58
- } catch {
59
- return undefined;
60
- }
61
- }
62
- function writeLastEffort(level: ReviewLevel): void {
63
- try {
64
- fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
65
- fs.writeFileSync(STATE_FILE, JSON.stringify({ codeReviewLastEffort: level }));
66
- } catch {
67
- /* ignore — non-critical */
68
- }
69
- }
70
-
71
- /**
72
- * Register the /code-review command — triggers the code-review skill.
73
- *
74
- * Handler decides the effective effort deterministically (CC 2.1.223): an
75
- * explicit level is persisted and used; with no level, the last-typed level is
76
- * reused; with no history, it falls back to low. The decision is announced in
77
- * the trigger message so it is observable — same pattern as /code-simplify.
78
- */
79
- export function registerCodeReview(pi: ExtensionAPI): void {
80
- pi.registerCommand("code-review", {
81
- description:
82
- "Review the current diff using the code-review skill. Usage: /code-review [low|medium|high|xhigh|max] [--fix] [--comment] [--share] [<pr#>|<branch>|<path>]",
83
- getArgumentCompletions(prefix) {
84
- const tokens = ["low", "medium", "high", "xhigh", "max", "--fix", "--comment", "--share"];
85
- return tokens.filter((t) => t.startsWith(prefix)).map((t) => ({ label: t, value: t }));
86
- },
87
- async handler(args) {
88
- const { level: explicit, rest } = parseReviewArgs(args ?? "");
89
- // Skip the read when we just wrote it — resolveEffort returns `explicit` unchanged.
90
- const lastUsed = explicit ? undefined : readLastEffort();
91
- if (explicit) writeLastEffort(explicit); // CC 2.1.223: remember the explicit level
92
- const { level, source } = resolveEffort(explicit, lastUsed);
93
- pi.sendUserMessage(
94
- `Run a code review now. Effective effort: ${level} (${source})${rest ? `; extra args: ${rest}` : ""}.\n\n` +
95
- `First load the review skill with the read tool: ${bundledSkillPath("code-review/SKILL.md")}. ` +
96
- `Then follow it exactly — use the \`subagent\` tool for any fan-out / verify / gap-hunt the skill calls for.`,
97
- );
98
- },
99
- });
100
- }
@@ -1,100 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { bundledSkillPath } from "../skills.ts";
5
-
6
- /** Context fraction at which we fall back to single-pass — a Pi-specific heuristic (see decideSimplifyMode). */
7
- const CONTEXT_NEAR_FULL_THRESHOLD = 0.8;
8
-
9
- export type SimplifyMode = "parallel" | "single-pass";
10
-
11
- /** Priority order for picking a verification command from package.json scripts. */
12
- const VERIFY_SCRIPT_PRIORITY = ["check", "test", "lint", "typecheck"] as const;
13
-
14
- /**
15
- * Pick the project verification command from a package.json `scripts` map, in
16
- * priority order (check → test → lint → typecheck). Pure — unit-testable.
17
- * Returns the runnable command (e.g. `npm run check`) or null when none exists.
18
- */
19
- export function detectVerifyCommand(scripts: Record<string, string> | null): string | null {
20
- if (!scripts) return null;
21
- for (const key of VERIFY_SCRIPT_PRIORITY) {
22
- const v = scripts[key];
23
- if (typeof v === "string" && v.trim() !== "") return `npm run ${key}`;
24
- }
25
- return null;
26
- }
27
-
28
- /** Read package.json scripts from `cwd`; returns null when absent/unparseable. */
29
- function readScriptsAt(cwd: string): Record<string, string> | null {
30
- try {
31
- const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8")) as {
32
- scripts?: Record<string, string>;
33
- };
34
- return pkg.scripts ?? null;
35
- } catch {
36
- return null;
37
- }
38
- }
39
-
40
- /**
41
- * Decide simplify mode deterministically from real context usage + tool availability.
42
- * Pure function — unit-testable.
43
- *
44
- * CC parity note: CC's /simplify guard (Dii, verified in the 2.1.227 binary) is
45
- * a SPAWN-DEPTH recursion limit, NOT a context check — `ok(ctx.agentContext) >= wV()`
46
- * where ok() returns the agent's depth (main=0) and wV() returns
47
- * CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH (default 3). That is N/A on Pi: the
48
- * `subagent` tool spawns a fresh subprocess (depth 0), so depth never accumulates.
49
- * The context-fraction heuristic below is a Pi-specific substitute (don't fan out
50
- * when the parent's context is near-full), NOT a mirror of Dii. The other Dii clause
51
- * — the Agent tool must be in the allowlist — IS mirrored here as `hasSubagent`.
52
- */
53
- export function decideSimplifyMode(opts: {
54
- tokens: number | null;
55
- contextWindow: number;
56
- hasSubagent: boolean;
57
- }): SimplifyMode {
58
- const { tokens, contextWindow, hasSubagent } = opts;
59
- // Conservative: if we can't measure context (tokens unknown / window 0) or the
60
- // subagent tool isn't registered, don't risk fan-out — go single-pass.
61
- if (tokens == null || contextWindow <= 0 || !hasSubagent) return "single-pass";
62
- const nearFull = tokens / contextWindow >= CONTEXT_NEAR_FULL_THRESHOLD;
63
- return nearFull ? "single-pass" : "parallel";
64
- }
65
-
66
- /**
67
- * Register the /code-simplify command. The handler decides parallel vs single-pass from
68
- * ctx.getContextUsage() (real token count) + subagent tool availability — this is the
69
- * deterministic Jvo guard that a pure-prompt skill cannot reproduce.
70
- */
71
- export function registerSimplify(pi: ExtensionAPI): void {
72
- pi.registerCommand("code-simplify", {
73
- description:
74
- "Clean up the changed code (reuse/simplification/efficiency/altitude) using the simplify skill. Mode (parallel 4-agent vs single-pass) is decided by the handler from real context usage. Usage: /code-simplify [<target>]",
75
- async handler(args, ctx) {
76
- const usage = ctx.getContextUsage();
77
- const hasSubagent = pi.getAllTools().some((t) => t.name === "subagent");
78
- const mode = decideSimplifyMode({
79
- tokens: usage?.tokens ?? null,
80
- contextWindow: usage?.contextWindow ?? 0,
81
- hasSubagent,
82
- });
83
- const pct = usage && usage.percent != null ? `${Math.round(usage.percent)}%` : "?";
84
- const bodyLabel = mode === "parallel" ? "PARALLEL MODE" : "SINGLE-PASS MODE";
85
- const verifyCmd = detectVerifyCommand(readScriptsAt(ctx.cwd));
86
- const verifyLine = verifyCmd
87
- ? `Verification command: \`${verifyCmd}\` (detected from package.json scripts). After applying Phase 2 fixes, run it; on failure, follow the skill's auto-revert procedure — never leave the working tree verified-broken.`
88
- : `No verification command detected in package.json (looked for check/test/lint/typecheck). Apply fixes and report outcomes, but state in the report that no verification was run (verification is opportunistic, never blocking).`;
89
- pi.sendUserMessage(
90
- `Clean up the changed code now. Target: ${args || "(whole diff)"}.\n\n` +
91
- `Handler decided ${mode} mode (context ${pct} full, subagent ${hasSubagent ? "available" : "absent"}). ` +
92
- `Load ${bundledSkillPath("simplify/SKILL.md")} via the read tool and follow the ${bodyLabel} body. ` +
93
- (mode === "parallel"
94
- ? `Use the \`subagent\` tool (mode: parallel) for the 4-agent fan-out.`
95
- : `Work the four angles inline — do not fake fan-out.`) +
96
- `\n${verifyLine}`,
97
- );
98
- },
99
- });
100
- }