@fyeeme/pi-review 2.0.0 → 2.0.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.
@@ -0,0 +1,238 @@
1
+ /**
2
+ * src/dispatch.ts — the generic prompt dispatcher (v2's only command layer).
3
+ *
4
+ * Per invocation: parse leading arguments, gather deterministic runtime
5
+ * variables (resolved diff via the candidate ladder, context usage,
6
+ * changed-file context package, sticky last-effort state), evaluate the
7
+ * guards declared in the selected template's frontmatter, pick the template
8
+ * variant, substitute {{var}} placeholders, and hand the rendered message to
9
+ * the session via sendUserMessage.
10
+ *
11
+ * The parallel-strategy decisions live in prompts/*.md frontmatter (data);
12
+ * this module only executes them. Adding a new prompt/skill requires no
13
+ * change here — the templates and skills are the registration surface.
14
+ */
15
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
+ import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
17
+ import { isFanoutToolAllowed } from "@fyeeme/pi-subagents";
18
+ import * as fs from "node:fs";
19
+ import * as os from "node:os";
20
+ import * as path from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ import { loadTurnBudgets } from "./config.ts";
23
+ import { DIFF_SCOPES, buildContextPackage, getRepoDiff, verifyLine } from "./diff.ts";
24
+ import { bundledSkillPath } from "./skills.ts";
25
+ import { parseGuards, selectVariant } from "./strategy.ts";
26
+
27
+ // This file lives at <pkg>/src/ → ".." is the package root.
28
+ const PKG_ROOT = fs.realpathSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
29
+
30
+ /** Absolute path to a prompt template bundled in this package's prompts/ dir. */
31
+ function bundledPromptPath(rel: string): string {
32
+ return path.join(PKG_ROOT, "prompts", rel);
33
+ }
34
+
35
+ /** Load a bundled template: frontmatter map + body. */
36
+ function loadTemplate(rel: string): { frontmatter: Record<string, unknown>; body: string } {
37
+ const { frontmatter, body } = parseFrontmatter<Record<string, unknown>>(
38
+ fs.readFileSync(bundledPromptPath(rel), "utf8"),
39
+ );
40
+ return { frontmatter, body };
41
+ }
42
+
43
+ /** Substitute {{var}} placeholders. An unknown placeholder is an error at
44
+ * load time in dev, but rendering must never crash a command — unfilled
45
+ * placeholders are left visible (they self-report in the rendered message).
46
+ * Pure — unit-testable. */
47
+ export function render(body: string, vars: Record<string, string>): string {
48
+ return body.replace(/\{\{([a-z-]+)\}\}/g, (whole, name: string) =>
49
+ name in vars ? vars[name]! : whole,
50
+ );
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // /review — effort-level code review (v1 command semantics, relocated)
55
+ // ---------------------------------------------------------------------------
56
+
57
+ /** Effort levels the /review command accepts (mirrors CC's effort enum). */
58
+ export const REVIEW_LEVELS = ["low", "medium", "high", "xhigh", "max"] as const;
59
+ export type ReviewLevel = (typeof REVIEW_LEVELS)[number];
60
+
61
+ const DEFAULT_LEVEL: ReviewLevel = "low";
62
+
63
+ /** Where the last explicitly-typed effort is persisted (CC 2.1.223 codeReviewLastEffort). */
64
+ const STATE_FILE = path.join(os.homedir(), ".pi", ".pi-review-state.json");
65
+
66
+ export type EffortSource = "explicit" | "last-used" | "default";
67
+
68
+ /**
69
+ * Parse a leading effort level out of raw args; the remainder (flags + target)
70
+ * is returned verbatim. Pure — unit-testable.
71
+ */
72
+ export function parseReviewArgs(args: string): { level: ReviewLevel | undefined; rest: string } {
73
+ const tokens = (args ?? "").trim().split(/\s+/).filter(Boolean);
74
+ if (tokens.length === 0) return { level: undefined, rest: "" };
75
+ const first = tokens[0]!.toLowerCase();
76
+ const isLevel = (REVIEW_LEVELS as readonly string[]).includes(first);
77
+ return {
78
+ level: isLevel ? (first as ReviewLevel) : undefined,
79
+ rest: isLevel ? tokens.slice(1).join(" ") : tokens.join(" "),
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Resolve the effective effort + where it came from. Pure — unit-testable.
85
+ * Explicit wins; otherwise the last-typed level; otherwise low.
86
+ */
87
+ export function resolveEffort(
88
+ explicit: ReviewLevel | undefined,
89
+ lastUsed: ReviewLevel | undefined,
90
+ ): { level: ReviewLevel; source: EffortSource } {
91
+ if (explicit) return { level: explicit, source: "explicit" };
92
+ if (lastUsed) return { level: lastUsed, source: "last-used" };
93
+ return { level: DEFAULT_LEVEL, source: "default" };
94
+ }
95
+
96
+ // Best-effort persistence — sticky-effort is a convenience, not a correctness
97
+ // invariant; a read/write failure must not break the review.
98
+ function readLastEffort(): ReviewLevel | undefined {
99
+ try {
100
+ const raw = JSON.parse(fs.readFileSync(STATE_FILE, "utf8")) as { codeReviewLastEffort?: unknown };
101
+ const v = raw.codeReviewLastEffort;
102
+ return typeof v === "string" && (REVIEW_LEVELS as readonly string[]).includes(v)
103
+ ? (v as ReviewLevel)
104
+ : undefined;
105
+ } catch {
106
+ return undefined;
107
+ }
108
+ }
109
+ function writeLastEffort(level: ReviewLevel): void {
110
+ try {
111
+ fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
112
+ fs.writeFileSync(STATE_FILE, JSON.stringify({ codeReviewLastEffort: level }));
113
+ } catch {
114
+ /* ignore — non-critical */
115
+ }
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // /simplify — cleanup fan-out with the declared parallel strategy
120
+ // ---------------------------------------------------------------------------
121
+
122
+ /** v1 DIFF_TOO_LARGE_CHARS — kept for the single-pass "too large to read at
123
+ * once" note (the strategy threshold itself lives in the template). */
124
+ const DIFF_TOO_LARGE_CHARS = 400_000;
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // Registration
128
+ // ---------------------------------------------------------------------------
129
+
130
+ export function registerDispatcher(pi: ExtensionAPI): void {
131
+ pi.registerCommand("review", {
132
+ description:
133
+ "Review the current diff using the review skill. Usage: /review [low|medium|high|xhigh|max] [--fix] [--comment] [--share] [<pr#>|<branch>|<path>]",
134
+ getArgumentCompletions(prefix) {
135
+ const tokens = ["low", "medium", "high", "xhigh", "max", "--fix", "--comment", "--share"];
136
+ return tokens.filter((t) => t.startsWith(prefix)).map((t) => ({ label: t, value: t }));
137
+ },
138
+ async handler(args, ctx) {
139
+ const { level: explicit, rest } = parseReviewArgs(args ?? "");
140
+ // Skip the read when we just wrote it — resolveEffort returns `explicit` unchanged.
141
+ const lastUsed = explicit ? undefined : readLastEffort();
142
+ if (explicit) writeLastEffort(explicit); // remember the explicit level
143
+ const { level, source } = resolveEffort(explicit, lastUsed);
144
+ const { body } = loadTemplate("review.md");
145
+ const budgets = loadTurnBudgets();
146
+ pi.sendUserMessage(
147
+ render(body, {
148
+ effort: level,
149
+ "effort-source": source,
150
+ "extra-args": rest ? `; extra args: ${rest}` : "",
151
+ skill: bundledSkillPath("review/SKILL.md"),
152
+ "finder-max-turns": String(budgets.subagent),
153
+ "verifier-max-turns": String(budgets.verifier),
154
+ "gap-hunt-max-turns": String(budgets.gapHunt),
155
+ // Consumed by the skill's --fix flow (apply → verify → re-report).
156
+ verify: verifyLine(ctx.cwd),
157
+ }),
158
+ );
159
+ },
160
+ });
161
+
162
+ pi.registerCommand("simplify", {
163
+ description:
164
+ "Clean up the changed code (reuse/simplification/efficiency/altitude) using the simplify skill. Mode (parallel 4-agent vs single-pass) is decided from the strategy declared in prompts/simplify.*.md (context usage, diff size, fan-out availability); PARALLEL opens with a visible Phase 0 before the subagent tool launches the agents. Usage: /simplify [<target>]",
165
+ async handler(args, ctx) {
166
+ try {
167
+ // ctx.signal (undefined while idle) lets Esc abort an in-flight diff.
168
+ const outcome = await getRepoDiff(ctx.cwd, args?.trim() || undefined, undefined, ctx.signal);
169
+ if (outcome.kind === "no-repo") {
170
+ ctx.ui.notify(`/simplify: ${ctx.cwd} is not inside a git repo — nothing to clean up.`, "warning");
171
+ return;
172
+ }
173
+ if (outcome.kind === "git-error") {
174
+ ctx.ui.notify(`/simplify: git failed — ${outcome.message}`, "error");
175
+ return;
176
+ }
177
+ if (outcome.kind === "empty") {
178
+ ctx.ui.notify(
179
+ `/simplify: no changes found (checked unpushed+uncommitted vs @{upstream}, uncommitted vs HEAD, staged, unstaged) — nothing to clean up.`,
180
+ "warning",
181
+ );
182
+ return;
183
+ }
184
+
185
+ const usage = ctx.getContextUsage();
186
+ const budgets = loadTurnBudgets(ctx.cwd);
187
+ const parallelTemplate = loadTemplate("simplify.parallel.md");
188
+ const { variant, reasons } = selectVariant(parseGuards(parallelTemplate.frontmatter), {
189
+ tokens: usage?.tokens ?? null,
190
+ contextWindow: usage?.contextWindow ?? 0,
191
+ diffChars: outcome.diff.length,
192
+ fanoutAvailable: isFanoutToolAllowed(),
193
+ });
194
+ const pct = usage && usage.percent != null ? `${Math.round(usage.percent)}%` : "?";
195
+ const target = args || "(whole diff)";
196
+ const skill = bundledSkillPath("simplify/SKILL.md");
197
+ const scopeLabel = DIFF_SCOPES[outcome.scopeKind];
198
+ const contextPackage = buildContextPackage(outcome.diff, outcome.gitRoot, scopeLabel);
199
+ const verify = verifyLine(outcome.gitRoot);
200
+
201
+ if (variant === "single-pass") {
202
+ const { body } = loadTemplate("simplify.single.md");
203
+ pi.sendUserMessage(
204
+ render(body, {
205
+ target,
206
+ reasons: reasons.join("; "),
207
+ "scope-label": scopeLabel,
208
+ "too-large":
209
+ outcome.diff.length >= DIFF_TOO_LARGE_CHARS
210
+ ? `\nThe diff is too large to read at once — work through it file-by-file from the changed-file list above.\n`
211
+ : "",
212
+ "git-command": outcome.gitCommand,
213
+ "context-package": contextPackage,
214
+ skill,
215
+ verify,
216
+ }),
217
+ );
218
+ return;
219
+ }
220
+
221
+ pi.sendUserMessage(
222
+ render(parallelTemplate.body, {
223
+ target,
224
+ pct,
225
+ "scope-label": scopeLabel,
226
+ "git-command": outcome.gitCommand,
227
+ "context-package": contextPackage,
228
+ skill,
229
+ verify,
230
+ "simplify-max-turns": String(budgets.simplify),
231
+ }),
232
+ );
233
+ } catch (err) {
234
+ ctx.ui.notify(`/simplify failed: ${err instanceof Error ? err.message : String(err)}`, "error");
235
+ }
236
+ },
237
+ });
238
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * src/strategy.ts — declarative parallel-strategy evaluation.
3
+ *
4
+ * v1's decideSimplifyMode hardcoded its thresholds in the command handler;
5
+ * v2 declares them as data in the prompt templates' frontmatter and this
6
+ * module evaluates the declared guards against runtime variables. Same
7
+ * semantics, different authority: editing a template's `parallel-when`
8
+ * block changes the strategy with no code change.
9
+ *
10
+ * Fallback-when-unguarded rules (v1 semantics, kept in code because they
11
+ * are safety invariants, not tuning knobs): unmeasurable context and an
12
+ * unavailable fan-out tool always select single-pass.
13
+ */
14
+
15
+ /** Guards declared in a template's `parallel-when` frontmatter block. */
16
+ export interface ParallelGuards {
17
+ /** Select single-pass when context usage is at or above this fraction. */
18
+ readonly contextBelow?: number;
19
+ /** Select single-pass when the diff reaches this many chars. */
20
+ readonly diffCharsBelow?: number;
21
+ }
22
+
23
+ /** Runtime variables gathered by the dispatcher. */
24
+ export interface StrategyRuntime {
25
+ readonly tokens: number | null;
26
+ readonly contextWindow: number;
27
+ readonly diffChars: number;
28
+ /** Whether fan-out tools are registered in this process (the recursion
29
+ * guard): single-pass is the only option when false. */
30
+ readonly fanoutAvailable: boolean;
31
+ }
32
+
33
+ export type ReviewVariant = "parallel" | "single-pass";
34
+
35
+ /**
36
+ * Evaluate the declared guards. Returns the variant plus the reasons that
37
+ * produced it (rendered into the trigger message so the decision stays
38
+ * observable). Pure — unit-testable.
39
+ */
40
+ export function selectVariant(
41
+ guards: ParallelGuards,
42
+ runtime: StrategyRuntime,
43
+ ): { variant: ReviewVariant; reasons: string[] } {
44
+ const { tokens, contextWindow, diffChars, fanoutAvailable } = runtime;
45
+ const reasons: string[] = [];
46
+ // Conservative safety invariant (not a tunable): if we can't measure
47
+ // context (tokens unknown / window 0), don't risk fan-out.
48
+ if (tokens == null || contextWindow <= 0) reasons.push("context usage unknown");
49
+ if (
50
+ guards.contextBelow != null &&
51
+ tokens != null &&
52
+ contextWindow > 0 &&
53
+ tokens / contextWindow >= guards.contextBelow
54
+ )
55
+ reasons.push(`context ${Math.round((tokens / contextWindow) * 100)}% full`);
56
+ if (guards.diffCharsBelow != null && diffChars >= guards.diffCharsBelow)
57
+ reasons.push(`diff too large (${Math.round(diffChars / 1024)} KB ≥ fan-out threshold)`);
58
+ if (!fanoutAvailable) reasons.push("fan-out unavailable in this context (subagent recursion guard)");
59
+ return { variant: reasons.length > 0 ? "single-pass" : "parallel", reasons };
60
+ }
61
+
62
+ /** Parse the `parallel-when` block out of a template's frontmatter map.
63
+ * Unknown/garbage fields are dropped (silent — garbage becomes absent). */
64
+ export function parseGuards(frontmatter: Record<string, unknown>): ParallelGuards {
65
+ const raw = frontmatter["parallel-when"];
66
+ if (!raw || typeof raw !== "object") return {};
67
+ const r = raw as Record<string, unknown>;
68
+ const out: { contextBelow?: number; diffCharsBelow?: number } = {};
69
+ if (typeof r["context-below"] === "number" && r["context-below"] > 0 && r["context-below"] <= 1) {
70
+ out.contextBelow = r["context-below"];
71
+ }
72
+ if (typeof r["diff-chars-below"] === "number" && r["diff-chars-below"] > 0) {
73
+ out.diffCharsBelow = r["diff-chars-below"];
74
+ }
75
+ return out;
76
+ }
@@ -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,25 @@ 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
34
  const OUTCOME_VALUES = ["fixed", "skipped", "no_change_needed"] as const;
33
35
  /** CC ReportFindings `outcome` 三档(2.1.227 二进制实证)。fixed-later 再上报时更新。 */
34
- const Outcome = Type.Union(OUTCOME_VALUES.map((v) => Type.Literal(v)));
36
+ const Outcome = StringEnum(OUTCOME_VALUES);
35
37
 
36
38
  // 供 SKILL-schema 同步测试引用(防漂移:SKILL 流程契约不得与常量脱节)。
37
39
  export { OUTCOME_VALUES, VERDICT_VALUES };
38
40
 
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"),
41
+ const Level = StringEnum([
42
+ "low",
43
+ "medium",
44
+ "high",
45
+ "xhigh",
46
+ "max",
45
47
  // simplify reuses this tool for structured apply-outcome reporting
46
48
  // (harden-code-simplify). Not a review effort level — carries no verdict.
47
- Type.Literal("simplify"),
48
- ]);
49
+ "simplify",
50
+ ] as const);
49
51
 
50
52
  // --- schema -----------------------------------------------------------------
51
53
 
@@ -223,12 +225,12 @@ export const reviewReportTool = defineTool<typeof ReviewReportParams, ReviewRepo
223
225
  name: "review_report",
224
226
  label: "Report review findings",
225
227
  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 对等物)。",
228
+ "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
229
  promptSnippet: "review_report — report structured code-review findings (renders Markdown + writes JSON for CI)",
228
230
  promptGuidelines: [
229
231
  "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
232
  "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.",
233
+ "Use `review_report` only when the code-review skill instructs reporting findings; otherwise follow the active output format.",
232
234
  ],
233
235
  parameters: ReviewReportParams,
234
236
 
@@ -267,7 +269,7 @@ export const reviewReportTool = defineTool<typeof ReviewReportParams, ReviewRepo
267
269
  let writeError: string | null = null;
268
270
  const now = new Date();
269
271
  try {
270
- const dir = path.join(ctx.cwd, ".pi", "review");
272
+ const dir = path.join(ctx.cwd, CONFIG_DIR_NAME, "review");
271
273
  await fs.promises.mkdir(dir, { recursive: true });
272
274
  const safeId = toolCallId.replace(/[^\w.-]+/g, "_");
273
275
  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
- }