@fyeeme/pi-review 1.0.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.
@@ -0,0 +1,157 @@
1
+ ---
2
+ name: simplify
3
+ description: "Review the changed code for reuse, simplification, efficiency, and altitude cleanups, then apply the fixes. Quality only — it does not hunt for bugs; use /code-review for that. v2 (from Claude Code CLI v2.1.223) — 4 cleanup agents fan out in parallel when context allows, else a single-pass inline cleanup; either way the fixes are applied to the working tree."
4
+ ---
5
+
6
+ <!--
7
+ Origin: Claude Code built-in skill `/simplify` (CLI v2.1.223), reverse-
8
+ engineered from bin/claude.exe strings. Pi registers it as /code-simplify.
9
+
10
+ Lineage:
11
+ v2.1.220 → the first reconstruction (v1)
12
+ v2.1.223 → verified 2026-08-06 against bin/claude.exe strings: skill body
13
+ (intro, Phase 0, the 4 cleanup angles, Phase 2 apply) and the
14
+ PARALLEL/SINGLE-PASS split are unchanged vs v2.1.220. The 4
15
+ angle bodies are shared verbatim with code-review's cleanup
16
+ angles (same source variables in the binary).
17
+
18
+ Bundled: ships inside the pi-review extension (skills/simplify/SKILL.md).
19
+
20
+ Invocation: /code-simplify [<target>]
21
+ target = file path | PR number | branch name
22
+
23
+ ════════════════════════════════════════════════════════════════════════
24
+ Pi ADAPTATIONS (differ from the CC runtime)
25
+ ════════════════════════════════════════════════════════════════════════
26
+ 1. Fan-out tool — CC uses the Agent tool; Pi uses the `subagent` tool
27
+ (mode: parallel). Where CC says "the Agent tool", read `subagent`.
28
+ 2. Mode guard — CC's _Yo has two clauses: (a) spawn-depth — single-pass when
29
+ agent depth >= CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH (default 3); (b) the
30
+ Agent tool must be in the allowlist. On Pi: (a) is N/A — the `subagent`
31
+ tool spawns a fresh subprocess (always depth 0), so depth never accumulates
32
+ — so decideSimplifyMode substitutes a context-fraction heuristic
33
+ (tokens/contextWindow >= 0.8 → single-pass), a Pi addition NOT a mirror of
34
+ _Yo; (b) is mirrored as "the `subagent` tool must be registered". The
35
+ decision is made DETERMINISTICALLY by the /code-simplify handler — it can
36
+ read ctx.getContextUsage(), which a pure-prompt skill cannot — and announced
37
+ in the trigger message; this skill just provides the two mode bodies.
38
+ 3. Command — CC: /simplify; Pi: /code-simplify.
39
+
40
+ Prerequisite: the `subagent` tool (provided by the pi-review extension) for
41
+ PARALLEL MODE. SINGLE-PASS MODE runs standalone.
42
+ -->
43
+
44
+ You are improving the quality of the changed code, not hunting for bugs. Review
45
+ it for reuse, simplification, efficiency, and altitude issues, then fix what you
46
+ find. Do not look for correctness bugs — that is what `/code-review` is for.
47
+
48
+ The `/code-simplify` handler has already chosen the mode (PARALLEL or
49
+ SINGLE-PASS) from real context usage and announced it in the trigger message.
50
+ Follow the body that matches; do not fake the mode you weren't asked to run.
51
+
52
+ ## Phase 0 — Gather the diff
53
+
54
+ Run `git diff @{upstream}...HEAD` (or `git diff main...HEAD` / `git diff HEAD~1`
55
+ if there's no upstream) to get the unified diff under review. If there are
56
+ uncommitted changes, or the range diff is empty, also run `git diff HEAD` and
57
+ include the working-tree changes in scope — the review often runs before the
58
+ commit. If a PR number, branch name, or file path was passed as an argument,
59
+ review that target instead. Treat this diff as the review scope.
60
+
61
+ ---
62
+
63
+ # PARALLEL MODE (subagent tool available AND context not near-full)
64
+
65
+ `/code-simplify → 4 cleanup agents in parallel → apply the fixes`
66
+
67
+ ## Phase 1 — Review (4 cleanup agents in parallel)
68
+
69
+ Launch **4 independent review agents** via the `subagent` tool, all in a single
70
+ message so they run concurrently (mode: parallel). Pass each agent the diff and
71
+ one of the four angles below. Each returns its findings with `file`, `line`, a
72
+ one-line `summary`, and the concrete cost (what is duplicated, wasted, or harder
73
+ to maintain).
74
+
75
+ ### Reuse
76
+ Flag new code that re-implements something the codebase already has — Grep
77
+ shared/utility modules and files adjacent to the change, and name the existing
78
+ helper to call instead.
79
+
80
+ ### Simplification
81
+ Flag unnecessary complexity the diff adds: redundant or derivable state,
82
+ copy-paste with slight variation, deep nesting, dead code left behind. Name the
83
+ simpler form that does the same job.
84
+
85
+ ### Efficiency
86
+ Flag wasted work the diff introduces: redundant computation or repeated I/O,
87
+ independent operations run sequentially, blocking work added to startup or hot
88
+ paths. Also flag long-lived objects built from closures or captured environments
89
+ — they keep the entire enclosing scope alive for the object's lifetime (a memory
90
+ leak when that scope holds large values); prefer a class/struct that copies only
91
+ the fields it needs. Name the cheaper alternative.
92
+
93
+ ### Altitude
94
+ Check that each change is implemented at the right depth, not as a fragile
95
+ bandaid. Special cases layered on shared infrastructure are a sign the fix isn't
96
+ deep enough — prefer generalizing the underlying mechanism over adding special
97
+ cases.
98
+
99
+ ## Phase 2 — Apply the fixes
100
+
101
+ Wait for all four agents to complete, dedup findings that point at the same line
102
+ or mechanism, and fix each remaining one directly. Skip any finding whose fix
103
+ would change intended behavior, require changes well outside the reviewed diff,
104
+ or that you judge to be a false positive — note the skip rather than arguing
105
+ with it. Finish with a brief summary of what was fixed and what was skipped (or
106
+ confirm the code was already clean).
107
+
108
+ ---
109
+
110
+ # SINGLE-PASS MODE (subagent tool unavailable OR context near-full)
111
+
112
+ `/code-simplify → subagent tool unavailable → single-pass inline cleanup → apply the fixes`
113
+
114
+ The `subagent` tool isn't available in this context (or context is near-full), so
115
+ the usual 4-agent fan-out can't run. Work through all four angles below yourself,
116
+ in this same context, in one pass — do not skip an angle for lack of fan-out.
117
+
118
+ ## Phase 1 — Review (4 cleanup angles, single pass)
119
+
120
+ Review the diff against each angle below in turn. For each, note findings with
121
+ `file`, `line`, a one-line `summary`, and the concrete cost (what is duplicated,
122
+ wasted, or harder to maintain).
123
+
124
+ ### Reuse
125
+ Flag new code that re-implements something the codebase already has — Grep
126
+ shared/utility modules and files adjacent to the change, and name the existing
127
+ helper to call instead.
128
+
129
+ ### Simplification
130
+ Flag unnecessary complexity the diff adds: redundant or derivable state,
131
+ copy-paste with slight variation, deep nesting, dead code left behind. Name the
132
+ simpler form that does the same job.
133
+
134
+ ### Efficiency
135
+ Flag wasted work the diff introduces: redundant computation or repeated I/O,
136
+ independent operations run sequentially, blocking work added to startup or hot
137
+ paths. Also flag long-lived objects built from closures or captured environments
138
+ — they keep the entire enclosing scope alive for the object's lifetime (a memory
139
+ leak when that scope holds large values); prefer a class/struct that copies only
140
+ the fields it needs. Name the cheaper alternative.
141
+
142
+ ### Altitude
143
+ Check that each change is implemented at the right depth, not as a fragile
144
+ bandaid. Special cases layered on shared infrastructure are a sign the fix isn't
145
+ deep enough — prefer generalizing the underlying mechanism over adding special
146
+ cases.
147
+
148
+ ## Phase 2 — Apply the fixes
149
+
150
+ Dedup findings that point at the same line or mechanism, and fix each remaining
151
+ one directly. Skip any finding whose fix would change intended behavior, require
152
+ changes well outside the reviewed diff, or that you judge to be a false positive
153
+ — note the skip rather than arguing with it. Finish with a brief summary of what
154
+ was fixed and what was skipped (or confirm the code was already clean). State
155
+ clearly in your summary that this was a single-pass review done without the
156
+ `subagent` tool, not the full 4-agent fan-out, so whoever reads it isn't misled
157
+ about what actually ran.
@@ -0,0 +1,353 @@
1
+ /**
2
+ * src/agent/dispatch.ts — agent dispatch via pi subprocess.
3
+ *
4
+ * Reuses the spawn pattern from examples/extensions/subagent and
5
+ * pi-dynamic-workflows/src/agent/dispatch.ts: one `pi --mode json -p
6
+ * --no-session` subprocess per agent call, stdout parsed for
7
+ * {message_end, tool_result_end} events, AbortSignal → SIGTERM with a
8
+ * 5s SIGKILL escalation.
9
+ *
10
+ * This is a SELF-CONTAINED copy (no pi-dynamic-workflows dependency): the
11
+ * workflow-engine retry/skip/lifecycle machinery is stripped, leaving only
12
+ * spawnAgent + mapWithConcurrencyLimit + a per-call abort registry. The
13
+ * subagent tool builds its single/parallel/chain modes on top of this.
14
+ *
15
+ * When pi promotes spawnAgent to a public @earendil-works/pi-coding-agent
16
+ * export, this file should be deleted in favor of that import.
17
+ */
18
+ import { spawn, type ChildProcess } from "node:child_process";
19
+ import { StringDecoder } from "node:string_decoder";
20
+ import * as fs from "node:fs";
21
+ import * as os from "node:os";
22
+ import * as path from "node:path";
23
+ import type { Message } from "@earendil-works/pi-ai";
24
+
25
+ /** Stable id for one agent call; the registry key for per-call abort. */
26
+ export type AgentCallId = string;
27
+
28
+ /** callId → per-call AbortController. */
29
+ export type AgentAbortMap = Map<AgentCallId, AbortController>;
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Concurrency limiter (ported from examples/extensions/subagent)
33
+ // ---------------------------------------------------------------------------
34
+
35
+ /**
36
+ * Run `fn` over `items` with at most `concurrency` in flight, preserving
37
+ * input order in the output array. parallel mode builds on this.
38
+ */
39
+ export async function mapWithConcurrencyLimit<TIn, TOut>(
40
+ items: TIn[],
41
+ concurrency: number,
42
+ fn: (item: TIn, index: number) => Promise<TOut>,
43
+ ): Promise<TOut[]> {
44
+ if (items.length === 0) return [];
45
+ const limit = Math.max(1, Math.min(concurrency, items.length));
46
+ const results: TOut[] = new Array(items.length);
47
+ let nextIndex = 0;
48
+ // Stop dispatching NEW items once any worker has errored, so a rejection
49
+ // doesn't leave sibling workers pulling more items and spawning unawaited
50
+ // subprocesses. In-flight calls finish; the failing worker rethrows.
51
+ let failed = false;
52
+ const workers = new Array(limit).fill(null).map(async () => {
53
+ while (!failed) {
54
+ const current = nextIndex++;
55
+ if (current >= items.length) return;
56
+ try {
57
+ results[current] = await fn(items[current], current);
58
+ } catch (err) {
59
+ failed = true;
60
+ throw err;
61
+ }
62
+ }
63
+ });
64
+ await Promise.all(workers);
65
+ return results;
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // pi binary resolution (ported from examples/extensions/subagent)
70
+ // ---------------------------------------------------------------------------
71
+
72
+ /**
73
+ * Resolve the `pi` invocation for the subprocess. Prefers re-entering the
74
+ * current script (node <script> / bun <script>); falls back to the `pi`
75
+ * binary on PATH when run under a generic runtime.
76
+ */
77
+ export function getPiInvocation(args: string[]): { command: string; args: string[] } {
78
+ const currentScript = process.argv[1];
79
+ const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
80
+ if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
81
+ return { command: process.execPath, args: [currentScript, ...args] };
82
+ }
83
+
84
+ const execName = path.basename(process.execPath).toLowerCase();
85
+ const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
86
+ if (!isGenericRuntime) {
87
+ return { command: process.execPath, args };
88
+ }
89
+
90
+ return { command: "pi", args };
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Usage + result
95
+ // ---------------------------------------------------------------------------
96
+
97
+ export interface AgentUsage {
98
+ input: number;
99
+ output: number;
100
+ cacheRead: number;
101
+ cacheWrite: number;
102
+ cost: number;
103
+ contextTokens: number;
104
+ turns: number;
105
+ }
106
+
107
+ export interface AgentSpawnOptions {
108
+ /** Stable id for this call; the registry key for per-call abort. */
109
+ readonly callId: AgentCallId;
110
+ /** Prompt passed as the final positional arg to `pi -p`. */
111
+ readonly task: string;
112
+ /** Working directory for the spawned pi process. Defaults to process.cwd(). */
113
+ readonly cwd?: string;
114
+ /** `--model` override. */
115
+ readonly model?: string;
116
+ /** `--tools` whitelist (comma-joined). */
117
+ readonly tools?: string[];
118
+ /** System prompt appended via a temp file (`--append-system-prompt`). */
119
+ readonly systemPrompt?: string;
120
+ /** Caller-level abort signal; linked to this call's per-call controller. */
121
+ readonly signal?: AbortSignal;
122
+ /** Max assistant turns. When reached, the subprocess is aborted (SIGTERM). Omit for unlimited. */
123
+ readonly maxTurns?: number;
124
+ }
125
+
126
+ export interface AgentSpawnResult {
127
+ callId: AgentCallId;
128
+ exitCode: number;
129
+ messages: Message[];
130
+ stderr: string;
131
+ usage: AgentUsage;
132
+ model?: string;
133
+ stopReason?: string;
134
+ errorMessage?: string;
135
+ /** True if aborted. exitCode may be null/non-zero. */
136
+ aborted: boolean;
137
+ /** True if killed because the caller's maxTurns budget was reached. Distinct
138
+ * from `aborted` (external cancel): the agent did useful bounded work. */
139
+ maxTurnsReached: boolean;
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Registry — Map<callId, ChildProcess> + per-call AbortController
144
+ // ---------------------------------------------------------------------------
145
+
146
+ export interface AgentSpawnRegistry {
147
+ /** callId → child process. The table that translates abort → SIGTERM on one process. */
148
+ readonly processes: Map<AgentCallId, ChildProcess>;
149
+ /** callId → per-call controller. */
150
+ readonly controllers: AgentAbortMap;
151
+ }
152
+
153
+ export function createSpawnRegistry(): AgentSpawnRegistry {
154
+ return {
155
+ processes: new Map(),
156
+ controllers: new Map(),
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Abort exactly one in-flight call by id. Aborts the call's per-call
162
+ * controller; spawnAgent's race-safe listener translates that into
163
+ * SIGTERM→SIGKILL on exactly the one subprocess. Returns false if the
164
+ * callId is not in flight.
165
+ */
166
+ export function abortAgent(registry: AgentSpawnRegistry, callId: AgentCallId): boolean {
167
+ const controller = registry.controllers.get(callId);
168
+ if (!controller) return false;
169
+ controller.abort();
170
+ return true;
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // Spawn — the dispatch primitive
175
+ // ---------------------------------------------------------------------------
176
+
177
+ export async function spawnAgent(
178
+ registry: AgentSpawnRegistry,
179
+ options: AgentSpawnOptions,
180
+ ): Promise<AgentSpawnResult> {
181
+ const { callId, task, cwd, model, tools, systemPrompt, signal } = options;
182
+
183
+ // Per-call controller — the abort entry point.
184
+ const controller = new AbortController();
185
+ registry.controllers.set(callId, controller);
186
+
187
+ // Link the caller-level signal to this call's controller so a run-wide
188
+ // abort reaches every in-flight call. Named + removed in finally — otherwise
189
+ // a normally-completing call leaks a listener on the parent signal.
190
+ const onParentAbort = (): void => controller.abort();
191
+ if (signal) {
192
+ if (signal.aborted) controller.abort();
193
+ else signal.addEventListener("abort", onParentAbort);
194
+ }
195
+
196
+ const args: string[] = ["--mode", "json", "-p", "--no-session"];
197
+ if (model) args.push("--model", model);
198
+ if (tools && tools.length > 0) args.push("--tools", tools.join(","));
199
+
200
+ let tmpPromptDir: string | null = null;
201
+ let tmpPromptPath: string | null = null;
202
+
203
+ const result: AgentSpawnResult = {
204
+ callId,
205
+ exitCode: 0,
206
+ messages: [],
207
+ stderr: "",
208
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
209
+ aborted: false,
210
+ maxTurnsReached: false,
211
+ };
212
+
213
+ try {
214
+ if (systemPrompt && systemPrompt.trim()) {
215
+ const tmp = await writePromptToTempFile(callId, systemPrompt);
216
+ tmpPromptDir = tmp.dir;
217
+ tmpPromptPath = tmp.filePath;
218
+ args.push("--append-system-prompt", tmpPromptPath);
219
+ }
220
+
221
+ // The prompt is the final positional arg consumed by `-p`.
222
+ args.push(task);
223
+
224
+ const exitCode = await new Promise<number>((resolve) => {
225
+ const invocation = getPiInvocation(args);
226
+ const proc = spawn(invocation.command, invocation.args, {
227
+ cwd: cwd ?? process.cwd(),
228
+ shell: false,
229
+ stdio: ["ignore", "pipe", "pipe"],
230
+ });
231
+ registry.processes.set(callId, proc);
232
+
233
+ let buffer = "";
234
+ const decoder = new StringDecoder("utf8");
235
+
236
+ const processLine = (line: string) => {
237
+ if (!line.trim()) return;
238
+ let event: { type: string; message?: Message };
239
+ try {
240
+ event = JSON.parse(line) as { type: string; message?: Message };
241
+ } catch {
242
+ return;
243
+ }
244
+
245
+ if (event.type === "message_end" && event.message) {
246
+ const msg = event.message;
247
+ result.messages.push(msg);
248
+ if (msg.role === "assistant") {
249
+ result.usage.turns++;
250
+ // Enforce maxTurns: abort the subprocess when the limit is reached.
251
+ // killProc handles the SIGTERM→SIGKILL escalation. Use `!= null` so an
252
+ // explicit maxTurns: 0 is honored (and marked) rather than treated as "unset".
253
+ if (options.maxTurns != null && result.usage.turns >= options.maxTurns) {
254
+ result.maxTurnsReached = true;
255
+ controller.abort();
256
+ }
257
+ const usage = msg.usage;
258
+ if (usage) {
259
+ result.usage.input += usage.input || 0;
260
+ result.usage.output += usage.output || 0;
261
+ result.usage.cacheRead += usage.cacheRead || 0;
262
+ result.usage.cacheWrite += usage.cacheWrite || 0;
263
+ result.usage.cost += Number(usage.cost?.total) || 0;
264
+ result.usage.contextTokens = usage.totalTokens || 0;
265
+ }
266
+ if (!result.model && msg.model) result.model = msg.model;
267
+ if (msg.stopReason) result.stopReason = msg.stopReason;
268
+ if (msg.errorMessage) result.errorMessage = msg.errorMessage;
269
+ }
270
+ }
271
+
272
+ if (event.type === "tool_result_end" && event.message) {
273
+ result.messages.push(event.message);
274
+ }
275
+ };
276
+
277
+ proc.stdout.on("data", (data) => {
278
+ // StringDecoder buffers incomplete multi-byte UTF-8 sequences across chunk
279
+ // boundaries so a CJK char split between two `data` events isn't replaced
280
+ // with U+FFFD (which would corrupt the line and silently drop the event).
281
+ buffer += decoder.write(data);
282
+ const lines = buffer.split("\n");
283
+ buffer = lines.pop() || "";
284
+ for (const line of lines) processLine(line);
285
+ });
286
+
287
+ proc.stderr.on("data", (data) => {
288
+ result.stderr += data.toString();
289
+ });
290
+
291
+ proc.on("close", (code) => {
292
+ const tail = decoder.end();
293
+ if (tail) buffer += tail;
294
+ if (buffer.trim()) processLine(buffer);
295
+ resolve(code ?? 1); // code===null → signal-killed (OOM/SIGKILL): treat as failure, not silent empty success
296
+ });
297
+
298
+ proc.on("error", (err) => {
299
+ // Surface the spawn error (e.g. ENOENT when `pi` is not on PATH) instead
300
+ // of swallowing it.
301
+ result.errorMessage = err.message;
302
+ result.stderr += err.message;
303
+ resolve(1);
304
+ });
305
+
306
+ // Per-call abort → SIGTERM (SIGKILL after 5s). Race-safe: if the controller
307
+ // was already aborted before this listener registered, kill now.
308
+ const killProc = () => {
309
+ // Late-abort guard: if the proc already exited, don't flip a successful
310
+ // result's `aborted` flag.
311
+ if (proc.exitCode !== null || proc.signalCode !== null) return;
312
+ result.aborted = true;
313
+ proc.kill("SIGTERM");
314
+ const timer = setTimeout(() => {
315
+ // SIGTERM may be ignored — force SIGKILL after the grace period.
316
+ proc.kill("SIGKILL");
317
+ }, 5000);
318
+ // Clear the timer once the proc exits so we don't leak a libuv handle.
319
+ proc.once("close", () => clearTimeout(timer));
320
+ };
321
+ if (controller.signal.aborted) killProc();
322
+ else controller.signal.addEventListener("abort", killProc, { once: true });
323
+ });
324
+
325
+ result.exitCode = exitCode;
326
+ return result;
327
+ } finally {
328
+ // Always release registry slots, the parent-signal listener, and temp files.
329
+ registry.processes.delete(callId);
330
+ registry.controllers.delete(callId);
331
+ if (signal) signal.removeEventListener("abort", onParentAbort);
332
+ if (tmpPromptPath)
333
+ try {
334
+ fs.unlinkSync(tmpPromptPath);
335
+ } catch {
336
+ /* ignore */
337
+ }
338
+ if (tmpPromptDir)
339
+ try {
340
+ fs.rmdirSync(tmpPromptDir);
341
+ } catch {
342
+ /* ignore */
343
+ }
344
+ }
345
+ }
346
+
347
+ async function writePromptToTempFile(callId: string, prompt: string): Promise<{ dir: string; filePath: string }> {
348
+ const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-cr-agent-"));
349
+ const safeName = callId.replace(/[^\w.-]+/g, "_");
350
+ const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
351
+ await fs.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
352
+ return { dir: tmpDir, filePath };
353
+ }
@@ -0,0 +1,100 @@
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
+ }
@@ -0,0 +1,66 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { bundledSkillPath } from "../skills.ts";
3
+
4
+ /** Context fraction at which we fall back to single-pass — a Pi-specific heuristic (see decideSimplifyMode). */
5
+ const CONTEXT_NEAR_FULL_THRESHOLD = 0.8;
6
+
7
+ export type SimplifyMode = "parallel" | "single-pass";
8
+
9
+ /**
10
+ * Decide simplify mode deterministically from real context usage + tool availability.
11
+ * Pure function — unit-testable.
12
+ *
13
+ * CC parity note: CC's /simplify guard (_Yo) is a SPAWN-DEPTH recursion limit, NOT a
14
+ * context check — `RO(ctx) >= dne()` where RO returns the agent's depth and dne()
15
+ * returns CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH (default 3). That is N/A on Pi: the
16
+ * `subagent` tool spawns a fresh subprocess (depth 0), so depth never accumulates.
17
+ * The context-fraction heuristic below is a Pi-specific substitute (don't fan out
18
+ * when the parent's context is near-full), NOT a mirror of _Yo. The other _Yo clause
19
+ * — the Agent tool must be in the allowlist — IS mirrored here as `hasSubagent`.
20
+ */
21
+ export function decideSimplifyMode(opts: {
22
+ tokens: number | null;
23
+ contextWindow: number;
24
+ hasSubagent: boolean;
25
+ }): SimplifyMode {
26
+ const { tokens, contextWindow, hasSubagent } = opts;
27
+ // Conservative: if we can't measure context (tokens unknown / window 0) or the
28
+ // subagent tool isn't registered, don't risk fan-out — go single-pass.
29
+ if (tokens == null || contextWindow <= 0 || !hasSubagent) return "single-pass";
30
+ const nearFull = tokens / contextWindow >= CONTEXT_NEAR_FULL_THRESHOLD;
31
+ return nearFull ? "single-pass" : "parallel";
32
+ }
33
+
34
+ /**
35
+ * Register the /code-simplify command. The handler decides parallel vs single-pass from
36
+ * ctx.getContextUsage() (real token count) + subagent tool availability — this is the
37
+ * deterministic Jvo guard that a pure-prompt skill cannot reproduce.
38
+ */
39
+ export function registerSimplify(pi: ExtensionAPI): void {
40
+ pi.registerCommand("code-simplify", {
41
+ description:
42
+ "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>]",
43
+ async handler(args, ctx) {
44
+ const usage = ctx.getContextUsage();
45
+ const hasSubagent = pi.getAllTools().some((t) => t.name === "subagent");
46
+ const mode = decideSimplifyMode({
47
+ tokens: usage?.tokens ?? null,
48
+ contextWindow: usage?.contextWindow ?? 0,
49
+ hasSubagent,
50
+ });
51
+ const pct =
52
+ usage && usage.tokens != null && usage.contextWindow > 0
53
+ ? Math.round((usage.tokens / usage.contextWindow) * 100) + "%"
54
+ : "?";
55
+ const bodyLabel = mode === "parallel" ? "PARALLEL MODE" : "SINGLE-PASS MODE";
56
+ pi.sendUserMessage(
57
+ `Clean up the changed code now. Target: ${args || "(whole diff)"}.\n\n` +
58
+ `Handler decided ${mode} mode (context ${pct} full, subagent ${hasSubagent ? "available" : "absent"}). ` +
59
+ `Load ${bundledSkillPath("simplify/SKILL.md")} via the read tool and follow the ${bodyLabel} body. ` +
60
+ (mode === "parallel"
61
+ ? `Use the \`subagent\` tool (mode: parallel) for the 4-agent fan-out.`
62
+ : `Work the four angles inline — do not fake fan-out.`),
63
+ );
64
+ },
65
+ });
66
+ }