@diegopetrucci/pi-extensions 0.1.76 → 0.1.78

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.
Files changed (53) hide show
  1. package/README.md +8 -8
  2. package/extensions/agent-workflow-audit/README.md +1 -1
  3. package/extensions/agent-workflow-audit/index.ts +277 -44
  4. package/extensions/agent-workflow-audit/package.json +1 -1
  5. package/extensions/annotate-git-diff/git.ts +0 -8
  6. package/extensions/annotate-git-diff/package.json +1 -1
  7. package/extensions/annotate-last-message/package.json +1 -1
  8. package/extensions/annotate-last-message/web/index.html +3 -3
  9. package/extensions/code-reviewer/README.md +5 -5
  10. package/extensions/code-reviewer/index.ts +258 -54
  11. package/extensions/code-reviewer/package.json +1 -1
  12. package/extensions/confirm-destructive/index.ts +2 -8
  13. package/extensions/confirm-destructive/package.json +1 -1
  14. package/extensions/context-cap/README.md +9 -9
  15. package/extensions/context-cap/index.ts +3 -3
  16. package/extensions/context-cap/package.json +2 -2
  17. package/extensions/contrarian/README.md +4 -3
  18. package/extensions/contrarian/index.ts +47 -40
  19. package/extensions/contrarian/package.json +1 -1
  20. package/extensions/dirty-repo-guard/index.ts +0 -4
  21. package/extensions/dirty-repo-guard/package.json +1 -1
  22. package/extensions/dynamic-context-pruning/README.md +5 -5
  23. package/extensions/dynamic-context-pruning/index.ts +114 -232
  24. package/extensions/dynamic-context-pruning/package.json +1 -1
  25. package/extensions/fast/README.md +2 -2
  26. package/extensions/fast/package.json +1 -1
  27. package/extensions/inline-bash/index.ts +0 -16
  28. package/extensions/inline-bash/package.json +1 -1
  29. package/extensions/librarian/README.md +1 -1
  30. package/extensions/librarian/index.ts +247 -39
  31. package/extensions/librarian/package.json +1 -1
  32. package/extensions/minimal-footer/README.md +5 -5
  33. package/extensions/minimal-footer/index.ts +3 -10
  34. package/extensions/minimal-footer/minimal-footer.example.json +1 -1
  35. package/extensions/minimal-footer/package.json +1 -1
  36. package/extensions/oracle/README.md +4 -3
  37. package/extensions/oracle/index.ts +45 -38
  38. package/extensions/oracle/package.json +1 -1
  39. package/extensions/permission-gate/README.md +3 -0
  40. package/extensions/permission-gate/index.ts +151 -28
  41. package/extensions/permission-gate/package.json +1 -1
  42. package/extensions/project-mcp-json/README.md +2 -2
  43. package/extensions/project-mcp-json/package.json +1 -1
  44. package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
  45. package/extensions/quiet-tools/README.md +30 -19
  46. package/extensions/quiet-tools/index.ts +233 -166
  47. package/extensions/quiet-tools/package.json +2 -2
  48. package/extensions/todo/index.ts +0 -19
  49. package/extensions/todo/package.json +1 -1
  50. package/extensions/triage-comments/README.md +4 -2
  51. package/extensions/triage-comments/index.ts +349 -29
  52. package/extensions/triage-comments/package.json +1 -1
  53. package/package.json +4 -4
package/README.md CHANGED
@@ -6,10 +6,10 @@ A collection of [pi](https://pi.dev) agent extensions I made. (Btw, [I am also b
6
6
 
7
7
  ### Subagents
8
8
 
9
- - [`code-reviewer`](./extensions/code-reviewer): Adds a read-only `code_reviewer` subagent tool that reviews local changes for ticket fit, diff mismatches, correctness, security, simplicity, and validation gaps without implementing fixes, auto-selects a contrarian-style opposite-provider review model when available, and clamps requested thinking to the chosen model's capabilities.
10
- - [`contrarian`](./extensions/contrarian): Adds a read-only contrarian tool that spins up an isolated subprocess to stress-test plans, designs, assumptions, bug hypotheses, and conclusions by steelmanning the strongest opposing case.
11
- - [`librarian`](./extensions/librarian): Adds a GitHub research scout with a local repo checkout cache disabled by default under the OS user cache directory, toggleable with `/librarian-cache`, configurable subagent model/thinking defaults via `/librarian-config`, and cached repos expiring after 7 days of non-use.
12
- - [`oracle`](./extensions/oracle): Adds an Amp-style read-only oracle tool that auto-selects the strongest reasoning model on the current provider/subscription, supports persisted `/oracle` model/thinking defaults, requests xhigh reasoning by default and clamps to model capabilities, and shows live status while running.
9
+ - [`code-reviewer`](./extensions/code-reviewer): Adds a `code_reviewer` subagent tool with read-only built-in tools plus configured MCP tools (run without confirmation, may have side effects, Pi >=1.0.4) that reviews local changes for ticket fit, diff mismatches, correctness, security, simplicity, and validation gaps without implementing fixes, auto-selects a contrarian-style opposite-provider review model when available, and clamps requested thinking to the chosen model's capabilities.
10
+ - [`contrarian`](./extensions/contrarian): Adds a contrarian tool with read-only built-in tools plus configured MCP tools (run without confirmation, may have side effects, Pi >=1.0.4) that spins up an isolated subprocess to stress-test plans, designs, assumptions, bug hypotheses, and conclusions by steelmanning the strongest opposing case.
11
+ - [`librarian`](./extensions/librarian): Adds a GitHub research scout with a local repo checkout cache disabled by default under the OS user cache directory, toggleable with `/librarian-cache`, configurable subagent model/thinking defaults via `/librarian-config`, cached repos expiring after 7 days of non-use, and configured MCP tools (run without confirmation, may have side effects, Pi >=1.0.4).
12
+ - [`oracle`](./extensions/oracle): Adds an Amp-style oracle tool with read-only built-in tools plus configured MCP tools (run without confirmation, may have side effects, Pi >=1.0.4) that auto-selects the strongest reasoning model on the current provider/subscription, supports persisted `/oracle` model/thinking defaults, requests xhigh reasoning by default and clamps to model capabilities, and shows live status while running.
13
13
 
14
14
  ### Providers
15
15
 
@@ -19,19 +19,19 @@ A collection of [pi](https://pi.dev) agent extensions I made. (Btw, [I am also b
19
19
 
20
20
  - [`git-footer`](./extensions/git-footer): Adds TLH-style git dirty counts, ahead/behind, and optional PR number to pi's built-in footer status area. This package is standalone-only and is not auto-loaded by the `@diegopetrucci/pi-extensions` collection package.
21
21
  - [`minimal-footer`](./extensions/minimal-footer): Replaces pi's built-in footer with a minimal configurable two-line layout: branch plus cached git dirty/ahead/PR status and repo on the first line, context/model on the second, optional `DUMB ZONE`, optional `xp` marker, plus OpenAI Codex usage windows labeled from their reported durations.
22
- - [`quiet-tools`](./extensions/quiet-tools): Renders collapsed built-in tool rows as a one-line invocation plus an expand hint without changing model-visible tool results; toggle temporarily with `/quiet-tools`.
22
+ - [`quiet-tools`](./extensions/quiet-tools): Always renders collapsed built-in tool rows and `mcp__*` rows as a one-line invocation plus an expand hint without changing model-visible tool results; inspect status with `/quiet-tools status`.
23
23
 
24
24
  ### / Slash commands
25
25
 
26
- - [`agent-workflow-audit`](./extensions/agent-workflow-audit): Adds `/agent-workflow-audit`, which runs an isolated repo workflow audit subagent and returns only the final distilled report to the main session.
26
+ - [`agent-workflow-audit`](./extensions/agent-workflow-audit): Adds `/agent-workflow-audit`, which runs an isolated repo workflow audit subagent with configured MCP tools (run without confirmation, may have side effects, Pi >=1.0.4) and returns only the final distilled report to the main session.
27
27
  - [`annotate-git-diff`](./extensions/annotate-git-diff): Adds `/annotate-git-diff`, a native Glimpse UI where explicit Submit sends structured feedback to the agent and close recovery keeps an unsent draft in the editor.
28
28
  - [`annotate-last-message`](./extensions/annotate-last-message): Adds `/annotate-last-message`, a native Glimpse UI for annotating the latest assistant reply and sending planning-oriented feedback directly to the agent.
29
29
  - [`review`](./extensions/review): Adds `/review` and `/end-review` for interactive code reviews of local changes, commits, PRs, and selected paths.
30
- - [`triage-comments`](./extensions/triage-comments): Adds `/triage-comments` and a read-only `triage_comments` subagent tool that can auto-detect the current branch's PR, filter resolved/outdated inline comments, classify selected review comments with evidence, and suggest handling options without implementing changes
30
+ - [`triage-comments`](./extensions/triage-comments): Adds `/triage-comments` and a `triage_comments` subagent tool with read-only built-in tools plus configured MCP tools (run without confirmation, may have side effects, Pi >=1.0.4) that can auto-detect the current branch's PR, filter resolved/outdated inline comments, classify selected review comments with evidence, and suggest handling options without implementing changes
31
31
 
32
32
  ### Context management
33
33
 
34
- - [`context-cap`](./extensions/context-cap): Caps effective model context windows at 200k tokens by default so pi avoids the `dumb zone`; toggle temporarily with `/context-cap`.
34
+ - [`context-cap`](./extensions/context-cap): Caps effective model context windows at 300k tokens by default so pi avoids the `dumb zone`; toggle temporarily with `/context-cap`.
35
35
  - [`context-inspector`](./extensions/context-inspector): Adds `/context`, a local self-contained HTML dashboard that breaks down where the current session context is going, with category overview, top offenders, and drilldown search.
36
36
  - [`dynamic-context-pruning`](./extensions/dynamic-context-pruning): Automatically prunes duplicate tool results, stale errored inputs, and superseded file-op outputs from long sessions behind a cache-aware net-benefit gate, with a `/prune` picker and `/context-pruning` status/control commands.
37
37
 
@@ -60,7 +60,7 @@ Options:
60
60
 
61
61
  - Runs a child `createAgentSession` with an in-memory session manager.
62
62
  - Disables inherited extensions, skills, prompt templates, themes, and context files in the child.
63
- - Gives the child `read`, `grep`, `find`, `ls`, and guarded `bash` tools.
63
+ - Gives the child `read`, `grep`, `find`, `ls`, and guarded `bash` tools, plus any configured MCP tools (run without confirmation; may have side effects; requires host Pi >=1.0.4; older hosts degrade to no MCP, no error); the global `mcp.json` always applies; a project `.pi/mcp.json` is loaded only when the host session has already trusted the project.
64
64
  - Returns a single final `agent-workflow-audit` custom message to the parent session.
65
65
  - Includes a short run-boundary footer noting that intermediate command output, errors, retries, and search path stayed in the child session.
66
66
 
@@ -1,5 +1,8 @@
1
1
  import * as fs from "node:fs/promises";
2
+ import { realpathSync } from "node:fs";
3
+ import * as os from "node:os";
2
4
  import * as path from "node:path";
5
+ import { fileURLToPath } from "node:url";
3
6
 
4
7
  import type {
5
8
  AgentSession,
@@ -15,6 +18,7 @@ import {
15
18
  getAgentDir,
16
19
  getMarkdownTheme,
17
20
  } from "@earendil-works/pi-coding-agent";
21
+ import * as piCodingAgent from "@earendil-works/pi-coding-agent";
18
22
  import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
19
23
 
20
24
  const CUSTOM_TYPE = "agent-workflow-audit";
@@ -204,11 +208,37 @@ function inspectFinalAssistant(messages: unknown[]): AuditAssistantOutcome {
204
208
  return { ok: false, reason: "Agent Workflow Audit subagent produced no assistant message" };
205
209
  }
206
210
 
207
- function isAbortLikeError(error: unknown): boolean {
208
- if (error && typeof error === "object" && (error as { name?: unknown }).name === "AbortError")
209
- return true;
210
- const message = error instanceof Error ? error.message : String(error);
211
- return /aborted|cancelled|canceled/i.test(message);
211
+ function classifyRunFailure(
212
+ error: unknown,
213
+ callerAborted: boolean,
214
+ ): { status: "aborted" | "error"; message: string; error?: string } {
215
+ const message = callerAborted
216
+ ? "Aborted"
217
+ : error instanceof Error
218
+ ? error.message
219
+ : String(error);
220
+ return {
221
+ status: callerAborted ? "aborted" : "error",
222
+ message,
223
+ error: callerAborted ? undefined : message,
224
+ };
225
+ }
226
+
227
+ function createAuditAbortController(abortChild: () => void, onCallerAbort: () => void) {
228
+ let callerAborted = false;
229
+ return {
230
+ get callerAborted() {
231
+ return callerAborted;
232
+ },
233
+ abortForCleanup() {
234
+ abortChild();
235
+ },
236
+ abortFromCaller() {
237
+ callerAborted = true;
238
+ onCallerAbort();
239
+ abortChild();
240
+ },
241
+ };
212
242
  }
213
243
 
214
244
  function renderCollapsedReport(report: string, lineLimit = COLLAPSED_REPORT_LINES): string {
@@ -267,6 +297,82 @@ async function gitStatusShort(
267
297
  return result.stdout.trim();
268
298
  }
269
299
 
300
+ const UNICODE_SPACES_PATTERN = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
301
+
302
+ /**
303
+ * Mirrors normalizeWindowsShellPath from dist/utils/paths.js. No-op on
304
+ * non-win32 paths.
305
+ */
306
+ function normalizeWindowsShellPathLocal(filePath: string): string {
307
+ if (!filePath.startsWith("/") || filePath.startsWith("//") || filePath.includes("\\"))
308
+ return filePath;
309
+ const match = filePath.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
310
+ if (!match) return filePath;
311
+ const suffix = match[2]?.replaceAll("/", "\\");
312
+ return `${match[1].toUpperCase()}:\\${suffix ?? ""}`;
313
+ }
314
+
315
+ /**
316
+ * Mirrors Pi's normalizePath (dist/utils/paths.js). On win32, Git Bash / WSL
317
+ * drive-path normalisation always runs; only tilde expansion is gated by
318
+ * expandTilde.
319
+ */
320
+ function normalizePath(
321
+ input: string,
322
+ opts: { normalizeUnicodeSpaces?: boolean; stripAtPrefix?: boolean; expandTilde?: boolean } = {},
323
+ ): string {
324
+ const { normalizeUnicodeSpaces = false, stripAtPrefix = false, expandTilde = true } = opts;
325
+ let p = input;
326
+ if (normalizeUnicodeSpaces) p = p.replace(UNICODE_SPACES_PATTERN, " ");
327
+ if (stripAtPrefix && p.startsWith("@")) p = p.slice(1);
328
+ if (process.platform === "win32") p = normalizeWindowsShellPathLocal(p);
329
+ if (expandTilde) {
330
+ const home = os.homedir();
331
+ if (p === "~") p = home;
332
+ else if (p.startsWith("~/") || (process.platform === "win32" && p.startsWith("~\\")))
333
+ p = path.join(home, p.slice(2));
334
+ }
335
+ if (/^file:\/\//.test(p)) p = fileURLToPath(p);
336
+ return p;
337
+ }
338
+
339
+ /**
340
+ * Mirrors Pi's resolveToCwd (dist/core/tools/path-utils.js). Exposed via
341
+ * __test__ for parity tests; not imported by other extensions.
342
+ */
343
+ function resolveToCwd(rawPath: string, cwd: string): string {
344
+ const normalized = normalizePath(rawPath, { normalizeUnicodeSpaces: true, stripAtPrefix: true });
345
+ const base = normalizePath(cwd);
346
+ return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(base, normalized);
347
+ }
348
+
349
+ /** Mirrors tryMacOSScreenshotPath from dist/core/tools/path-utils.js */
350
+ function tryMacOSScreenshotPathVariant(filePath: string): string {
351
+ return filePath.replace(/ (AM|PM)\./gi, " $1.");
352
+ }
353
+ /** Mirrors tryNFDVariant from dist/core/tools/path-utils.js */
354
+ function tryNFDPathVariant(filePath: string): string {
355
+ return filePath.normalize("NFD");
356
+ }
357
+ /** Mirrors tryCurlyQuoteVariant from dist/core/tools/path-utils.js */
358
+ function tryCurlyQuotePathVariant(filePath: string): string {
359
+ return filePath.replace(/'/g, "’");
360
+ }
361
+ /**
362
+ * Returns all read path candidates including Pi's resolveReadPathAsync fallback
363
+ * variants. Used only for the read tool guard; grep/find/ls use resolveToCwd only.
364
+ */
365
+ function getReadPathCandidates(resolved: string): string[] {
366
+ const nfd = tryNFDPathVariant(resolved);
367
+ return [
368
+ resolved,
369
+ tryMacOSScreenshotPathVariant(resolved),
370
+ nfd,
371
+ tryCurlyQuotePathVariant(resolved),
372
+ tryCurlyQuotePathVariant(nfd),
373
+ ];
374
+ }
375
+
270
376
  function resolveToolPath(cwd: string, rawPath: unknown): string {
271
377
  const input = typeof rawPath === "string" && rawPath.trim() ? rawPath.trim() : ".";
272
378
  const normalized = input.startsWith("@") ? input.slice(1) : input;
@@ -288,11 +394,33 @@ async function assertToolPathInsideCwd(
288
394
  ): Promise<string | undefined> {
289
395
  if (rawPath !== undefined && typeof rawPath !== "string")
290
396
  return `${toolName} path must be a string.`;
397
+ // A path is allowed if it resolves inside EITHER the realpath'd root (root) OR
398
+ // the lexical cwd (lexicalCwd). This handles missing files in symlinked workspaces
399
+ // where realpath fails and the lexical fallback starts with the alias prefix.
291
400
  const root = await fs.realpath(cwd).catch(() => path.resolve(cwd));
401
+ const lexicalCwd = path.resolve(cwd);
292
402
  const resolved = resolveToolPath(cwd, rawPath);
293
403
  const realPath = await fs.realpath(resolved).catch(() => resolved);
294
- if (!isInside(root, realPath))
404
+ if (!isInside(root, realPath) && !isInside(lexicalCwd, realPath))
295
405
  return `${toolName} is limited to the audited checkout: ${realPath}`;
406
+ // Pi-effective path check (monotonic: block if either resolution is outside).
407
+ // For the read tool, also check Pi's resolveReadPathAsync fallback variants so
408
+ // a missing file whose variant resolves outside is not silently allowed.
409
+ try {
410
+ const piResolved = resolveToCwd(typeof rawPath === "string" ? rawPath : ".", cwd);
411
+ const candidates = toolName === "read" ? getReadPathCandidates(piResolved) : [piResolved];
412
+ // Deliberate policy: all candidates are checked regardless of existence (not
413
+ // Pi's existence-ordered selection) so the guard cannot be raced by creating
414
+ // a variant between the hook and the read. Trade-off: a read is blocked when
415
+ // an escaping variant symlink sits next to a safe primary file.
416
+ for (const candidate of candidates) {
417
+ const candidateRealPath = await fs.realpath(candidate).catch(() => candidate);
418
+ if (!isInside(root, candidateRealPath) && !isInside(lexicalCwd, candidateRealPath))
419
+ return `${toolName} is limited to the audited checkout: ${candidateRealPath}`;
420
+ }
421
+ } catch {
422
+ return `${toolName} path could not be resolved safely.`;
423
+ }
296
424
  return undefined;
297
425
  }
298
426
 
@@ -330,6 +458,93 @@ const SAFE_DIRECT_COMMANDS = new Set([
330
458
  "yarn",
331
459
  ]);
332
460
 
461
+ const SUBAGENT_MCP_TOOLS = [
462
+ "mcp__*",
463
+ "tool_search",
464
+ "list_mcp_resources",
465
+ "list_mcp_resource_templates",
466
+ "read_mcp_resource",
467
+ ];
468
+
469
+ // Derive project-trust for the isolated subagent session from the host tool context.
470
+ // Fails closed: returns false if the host has not trusted the project, or if the subagent
471
+ // runs in a different directory, preventing an untrusted project's .pi/mcp.json from
472
+ // being loaded by the MCP extension.
473
+ function resolveSubagentProjectTrusted(
474
+ ctx: { cwd: string; isProjectTrusted(): boolean },
475
+ subagentCwd: string,
476
+ ): boolean {
477
+ // Feature-detected for older Pi hosts that may lack isProjectTrusted.
478
+ const hostTrusted =
479
+ typeof (ctx as { isProjectTrusted?: unknown }).isProjectTrusted === "function"
480
+ ? ctx.isProjectTrusted()
481
+ : false;
482
+ if (!hostTrusted) return false;
483
+ try {
484
+ return realpathSync(subagentCwd) === realpathSync(ctx.cwd);
485
+ } catch {
486
+ return false; // fail closed if realpath resolution fails
487
+ }
488
+ }
489
+
490
+ /** Returns true when v is a semver string >=1.0.4 (ignores prerelease; missing/garbage -> false). */
491
+ function isMcpCompatibleVersion(v: string | undefined): boolean {
492
+ if (!v) return false;
493
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
494
+ if (!m) return false;
495
+ const major = Number(m[1]),
496
+ minor = Number(m[2]),
497
+ patch = Number(m[3]);
498
+ if (major !== 1) return major > 1;
499
+ if (minor !== 0) return minor > 0;
500
+ return patch >= 4;
501
+ }
502
+
503
+ // Safe runtime access to MCP/tool-search factories — feature-detected for Pi >=0.99.0 hosts.
504
+ // Additionally gated on VERSION >=1.0.4: on older hosts mcp__* allowlist entries are exact-name
505
+ // matches that resolve nothing, so spawning MCP servers provides no benefit.
506
+ function createMcpSubagentFactories(): ExtensionFactory[] {
507
+ const ns = piCodingAgent as Record<string, unknown>;
508
+ if (!isMcpCompatibleVersion(ns["VERSION"] as string | undefined)) return [];
509
+ const factories: ExtensionFactory[] = [];
510
+ const mcp = ns["createMcpExtension"];
511
+ const toolSearch = ns["createToolSearchExtension"];
512
+ if (typeof mcp === "function") factories.push((mcp as () => ExtensionFactory)());
513
+ if (typeof toolSearch === "function") factories.push((toolSearch as () => ExtensionFactory)());
514
+ return factories;
515
+ }
516
+
517
+ // Emit session_shutdown before dispose so MCP closes server connections (no leaked child processes).
518
+ async function disposeSubagentSession(session: AgentSession | undefined): Promise<void> {
519
+ if (!session) return;
520
+ try {
521
+ const runner = (
522
+ session as unknown as {
523
+ extensionRunner?: {
524
+ hasHandlers(n: string): boolean;
525
+ emit(e: { type: string; reason: string }): Promise<unknown>;
526
+ };
527
+ }
528
+ ).extensionRunner;
529
+ if (runner?.hasHandlers("session_shutdown")) {
530
+ await runner.emit({ type: "session_shutdown", reason: "quit" });
531
+ }
532
+ } catch {
533
+ // Shutdown errors must not prevent dispose.
534
+ }
535
+ session.dispose();
536
+ }
537
+
538
+ function isSubagentMcpTool(toolName: string): boolean {
539
+ return (
540
+ toolName.startsWith("mcp__") ||
541
+ toolName === "tool_search" ||
542
+ toolName === "list_mcp_resources" ||
543
+ toolName === "list_mcp_resource_templates" ||
544
+ toolName === "read_mcp_resource"
545
+ );
546
+ }
547
+
333
548
  const READ_ONLY_GIT_SUBCOMMANDS = new Set([
334
549
  "blame",
335
550
  "branch",
@@ -677,9 +892,7 @@ function getBlockedPackageManagerReason(commandName: string, tokens: string[]):
677
892
  index += 1;
678
893
  }
679
894
  }
680
- const runnerIndex = lowerTokens.findIndex((token) =>
681
- ["node", "python", "python3", "ruby", "php"].includes(token),
682
- );
895
+ const runnerIndex = lowerTokens.findIndex((token) => ["node", "ruby", "php"].includes(token));
683
896
  if (runnerIndex >= 0) {
684
897
  const runnerReason = getBlockedInterpreterReason(
685
898
  lowerTokens[runnerIndex],
@@ -961,9 +1174,6 @@ function getBlockedInterpreterReason(commandName: string, tokens: string[]): str
961
1174
  ) {
962
1175
  return `Agent Workflow Audit blocks inline/preload ${commandName} execution; run documented project scripts instead.`;
963
1176
  }
964
- if (["python", "python3"].includes(commandName) && hasInlineFlag(lowerTokens, ["-c"])) {
965
- return "Agent Workflow Audit blocks inline Python execution; run documented project scripts instead.";
966
- }
967
1177
  if (["ruby", "php"].includes(commandName) && hasInlineFlag(lowerTokens, ["-e", "-r"])) {
968
1178
  return `Agent Workflow Audit blocks inline/preload ${commandName} execution; run documented project scripts instead.`;
969
1179
  }
@@ -1137,10 +1347,13 @@ function createAuditRuntimeGuardExtension(options: {
1137
1347
  });
1138
1348
 
1139
1349
  pi.on("tool_call", async (event) => {
1140
- if (!["read", "grep", "find", "ls", "bash"].includes(event.toolName)) {
1350
+ if (
1351
+ !["read", "grep", "find", "ls", "bash"].includes(event.toolName) &&
1352
+ !isSubagentMcpTool(event.toolName)
1353
+ ) {
1141
1354
  return {
1142
1355
  block: true,
1143
- reason: `agent-workflow-audit exposes only read, grep, find, ls, and guarded bash; ${event.toolName} is not allowed.`,
1356
+ reason: `agent-workflow-audit exposes only read, grep, find, ls, guarded bash, and MCP tools; ${event.toolName} is not allowed.`,
1144
1357
  };
1145
1358
  }
1146
1359
 
@@ -1212,7 +1425,7 @@ function buildSystemPrompt(options: {
1212
1425
  ? "Plan-only mode is active. Do not run project commands with bash. Read instructions and manifests, infer the likely workflow, and report what would be tried plus remaining uncertainty."
1213
1426
  : "Execution mode is active. Try documented setup, build, lint, test, run, and other obvious project commands when they are safe and relevant. A runtime guard blocks deploy/publish/VCS-mutating/destructive commands; if a command is blocked, report it as a workflow/safety finding instead of trying to bypass it.";
1214
1427
 
1215
- return `You are Agent Workflow Audit, an isolated subagent running inside The Last Harness. Your job is to stress-test how efficiently an agent can operate in the current repository and return a concise final audit report to the parent session.
1428
+ return `You are Agent Workflow Audit, an isolated subagent. Your job is to stress-test how efficiently an agent can operate in the current repository and return a concise final audit report to the parent session.
1216
1429
 
1217
1430
  Working directory: ${options.cwd}
1218
1431
  Turn budget: ${options.maxTurns} turns total, including your final answer.
@@ -1233,7 +1446,7 @@ Workflow:
1233
1446
 
1234
1447
  Constraints:
1235
1448
  - Do not fix application code or failing tests. Do not edit, write, move, delete, commit, checkout, reset, clean, push, publish, deploy, or mutate GitHub.
1236
- - Use built-in read/grep/find/ls for file inspection. Use bash only for safe documented project commands or read-only local inspection.
1449
+ - Use built-in read/grep/find/ls for file inspection. Use bash only for safe documented project commands or read-only local inspection. Configured MCP tools are also available and run without confirmation; use them only to gather evidence or context, not for state-changing actions unless the task explicitly requires it. MCP access requires host Pi >=1.0.4; on older hosts MCP tools are absent with no error.
1237
1450
  - Do not paste raw full logs. Summarize command output and include short excerpts only when needed to prove a finding.
1238
1451
  - Never present a command as successful unless tool output showed success.
1239
1452
  - Mark inferred steps as assumptions.
@@ -1354,27 +1567,31 @@ async function runAudit(
1354
1567
  let unsubscribe: (() => void) | undefined;
1355
1568
  let runTimeout: NodeJS.Timeout | undefined;
1356
1569
  let abortListenerAdded = false;
1357
- let aborted = Boolean(ctx.signal?.aborted);
1358
1570
 
1359
1571
  const emit = () => updateAuditUi(ctx, details, lastContent);
1360
- const abort = () => {
1361
- aborted = true;
1362
- details.status = "aborted";
1363
- details.endedAt = Date.now();
1364
- lastContent = "Aborted";
1365
- emit();
1366
- void session?.abort();
1367
- };
1572
+ const abortController = createAuditAbortController(
1573
+ () => {
1574
+ void session?.abort();
1575
+ },
1576
+ () => {
1577
+ details.status = "aborted";
1578
+ details.endedAt = Date.now();
1579
+ lastContent = "Aborted";
1580
+ emit();
1581
+ },
1582
+ );
1583
+ const abortFromCaller = () => abortController.abortFromCaller();
1368
1584
 
1369
- if (ctx.signal?.aborted) abort();
1585
+ if (ctx.signal?.aborted) abortFromCaller();
1370
1586
  if (ctx.signal && !ctx.signal.aborted) {
1371
- ctx.signal.addEventListener("abort", abort);
1587
+ ctx.signal.addEventListener("abort", abortFromCaller);
1372
1588
  abortListenerAdded = true;
1373
1589
  }
1374
1590
 
1375
1591
  try {
1376
1592
  emit();
1377
- const isolatedSettingsManager = SettingsManager.inMemory({});
1593
+ const projectTrusted = resolveSubagentProjectTrusted(ctx, cwd);
1594
+ const isolatedSettingsManager = SettingsManager.inMemory({}, { projectTrusted });
1378
1595
  const resourceLoader = new DefaultResourceLoader({
1379
1596
  cwd,
1380
1597
  agentDir: getAgentDir(),
@@ -1386,6 +1603,7 @@ async function runAudit(
1386
1603
  noContextFiles: true,
1387
1604
  extensionFactories: [
1388
1605
  createAuditRuntimeGuardExtension({ cwd, maxTurns: MAX_TURNS, planOnly: options.planOnly }),
1606
+ ...createMcpSubagentFactories(),
1389
1607
  ],
1390
1608
  systemPromptOverride: () =>
1391
1609
  buildSystemPrompt({
@@ -1404,8 +1622,8 @@ async function runAudit(
1404
1622
  await resourceLoader.reload();
1405
1623
 
1406
1624
  const tools = options.planOnly
1407
- ? ["read", "grep", "find", "ls"]
1408
- : ["read", "grep", "find", "ls", "bash"];
1625
+ ? ["read", "grep", "find", "ls", ...SUBAGENT_MCP_TOOLS]
1626
+ : ["read", "grep", "find", "ls", "bash", ...SUBAGENT_MCP_TOOLS];
1409
1627
  const created = await createAgentSession({
1410
1628
  cwd,
1411
1629
  ...getModelRuntimeOption(ctx),
@@ -1417,9 +1635,17 @@ async function runAudit(
1417
1635
  tools,
1418
1636
  });
1419
1637
 
1638
+ // Assign session BEFORE bindExtensions so the finally block can dispose it
1639
+ // even if binding fails partway through (e.g. a server starts then an
1640
+ // extension's session_start throws).
1420
1641
  session = created.session;
1642
+ // Connect extensions (MCP servers) before prompting. Feature-detected: Pi >=0.99.0.
1643
+ await (created.session as any).bindExtensions?.({ mode: "json" });
1421
1644
  unsubscribe = session.subscribe((event) => {
1422
1645
  switch (event.type) {
1646
+ case "message_start":
1647
+ if (event.message.role === "assistant") lastContent = "";
1648
+ break;
1423
1649
  case "message_update":
1424
1650
  if (event.assistantMessageEvent?.type === "text_delta") {
1425
1651
  lastContent += event.assistantMessageEvent.delta ?? "";
@@ -1454,7 +1680,7 @@ async function runAudit(
1454
1680
  }
1455
1681
  });
1456
1682
 
1457
- if (!aborted) {
1683
+ if (!abortController.callerAborted) {
1458
1684
  const promptPromise = session.prompt(
1459
1685
  buildUserPrompt({
1460
1686
  cwd,
@@ -1466,12 +1692,11 @@ async function runAudit(
1466
1692
  );
1467
1693
  const timeoutPromise = new Promise<never>((_resolve, reject) => {
1468
1694
  runTimeout = setTimeout(() => {
1469
- abort();
1470
- reject(
1471
- new Error(
1472
- `/agent-workflow-audit timed out after ${Math.round(MAX_RUN_MS / 1000)} seconds.`,
1473
- ),
1695
+ const timeoutError = new Error(
1696
+ `/agent-workflow-audit timed out after ${Math.round(MAX_RUN_MS / 1000)} seconds.`,
1474
1697
  );
1698
+ reject(timeoutError);
1699
+ abortController.abortForCleanup();
1475
1700
  }, MAX_RUN_MS);
1476
1701
  });
1477
1702
  await Promise.race([promptPromise, timeoutPromise]);
@@ -1486,7 +1711,7 @@ async function runAudit(
1486
1711
  if (outcome.ok) {
1487
1712
  lastContent = outcome.answer;
1488
1713
  details.status = "done";
1489
- } else if (aborted) {
1714
+ } else if (abortController.callerAborted) {
1490
1715
  lastContent = "Aborted";
1491
1716
  details.status = "aborted";
1492
1717
  } else {
@@ -1497,10 +1722,10 @@ async function runAudit(
1497
1722
  emit();
1498
1723
  return { report: lastContent, details };
1499
1724
  } catch (error) {
1500
- const wasAbort = aborted || isAbortLikeError(error);
1501
- const message = wasAbort ? "Aborted" : error instanceof Error ? error.message : String(error);
1502
- details.status = wasAbort ? "aborted" : "error";
1503
- details.error = wasAbort ? undefined : message;
1725
+ const failure = classifyRunFailure(error, abortController.callerAborted);
1726
+ const { message } = failure;
1727
+ details.status = failure.status;
1728
+ details.error = failure.error;
1504
1729
  details.endedAt = Date.now();
1505
1730
  lastContent = message;
1506
1731
  details.reportLength = lastContent.length;
@@ -1508,18 +1733,26 @@ async function runAudit(
1508
1733
  return { report: `## Agent Workflow Audit failed\n\n${message}`, details };
1509
1734
  } finally {
1510
1735
  if (runTimeout) clearTimeout(runTimeout);
1511
- if (ctx.signal && abortListenerAdded) ctx.signal.removeEventListener("abort", abort);
1736
+ if (ctx.signal && abortListenerAdded) ctx.signal.removeEventListener("abort", abortFromCaller);
1512
1737
  unsubscribe?.();
1513
- session?.dispose();
1738
+ await disposeSubagentSession(session);
1514
1739
  }
1515
1740
  }
1516
1741
 
1517
1742
  export const __test__ = {
1743
+ SUBAGENT_MCP_TOOLS,
1744
+ isMcpCompatibleVersion,
1745
+ resolveSubagentProjectTrusted,
1746
+ createMcpSubagentFactories,
1747
+ disposeSubagentSession,
1748
+ classifyRunFailure,
1749
+ createAuditAbortController,
1750
+ createAuditRuntimeGuardExtension,
1518
1751
  appendRunBoundary,
1519
1752
  assertToolPathInsideCwd,
1753
+ resolveToCwd,
1520
1754
  buildSystemPrompt,
1521
1755
  buildUserPrompt,
1522
- createAuditRuntimeGuardExtension,
1523
1756
  inspectFinalAssistant,
1524
1757
  formatToolCall,
1525
1758
  getBlockedBashReason,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-agent-workflow-audit",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "A pi extension that runs an isolated repo workflow audit and returns only the final report to the main session.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -381,20 +381,14 @@ const binaryExtensions = new Set([
381
381
  ".7z",
382
382
  ".a",
383
383
  ".avi",
384
- ".avif",
385
384
  ".bin",
386
- ".bmp",
387
385
  ".class",
388
386
  ".dll",
389
387
  ".dylib",
390
388
  ".eot",
391
389
  ".exe",
392
- ".gif",
393
390
  ".gz",
394
- ".ico",
395
391
  ".jar",
396
- ".jpeg",
397
- ".jpg",
398
392
  ".lockb",
399
393
  ".map",
400
394
  ".mov",
@@ -403,7 +397,6 @@ const binaryExtensions = new Set([
403
397
  ".o",
404
398
  ".otf",
405
399
  ".pdf",
406
- ".png",
407
400
  ".pyc",
408
401
  ".so",
409
402
  ".svgz",
@@ -411,7 +404,6 @@ const binaryExtensions = new Set([
411
404
  ".ttf",
412
405
  ".wasm",
413
406
  ".webm",
414
- ".webp",
415
407
  ".woff",
416
408
  ".woff2",
417
409
  ".zip",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-annotate-git-diff",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "A standalone pi extension that adds /annotate-git-diff, a native Glimpse UI for annotating git diffs and sending explicit feedback directly to the agent; close recovery keeps unsent drafts in the editor.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-annotate-last-message",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "A standalone pi extension that adds /annotate-last-message, a native Glimpse UI for annotating the latest assistant reply.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -10,8 +10,8 @@
10
10
  * overrides semantic and Markdown colours from ctx.ui.theme. When Pi's theme
11
11
  * cannot provide a token, the matching dark/light fallback remains readable.
12
12
  *
13
- * The --md* fallback values below are kept in sync with the FALLBACK
14
- * object in theme.ts so this template renders correctly when the injected
13
+ * The --md* fallback values below are kept in sync with DARK_FALLBACK
14
+ * in theme.ts so this template renders correctly when the injected
15
15
  * theme block is absent (e.g. opened standalone in a browser for debugging).
16
16
  */
17
17
  :root {
@@ -31,7 +31,7 @@
31
31
  --error: #ffb3b3;
32
32
  --warning: #f4c95d;
33
33
 
34
- /* Markdown token colours — kept in sync with FALLBACK in theme.ts */
34
+ /* Markdown token colours — kept in sync with DARK_FALLBACK in theme.ts */
35
35
  --mdHeading: #f4c95d;
36
36
  --mdLink: #7dd3fc;
37
37
  --mdLinkUrl: #8a8a8a;
@@ -1,10 +1,8 @@
1
1
  # code-reviewer
2
2
 
3
- Adds a read-only `code_reviewer` subagent tool that adapts The Last Harness [`code-reviewer`](https://github.com/diegopetrucci/the-last-harness/blob/main/agents/subagents/code-reviewer.md) prompt into a standalone pi extension.
3
+ Adds a `code_reviewer` subagent tool that adapts The Last Harness [`code-reviewer`](https://github.com/diegopetrucci/the-last-harness/blob/main/agents/subagents/code-reviewer.md) prompt into a standalone pi extension.
4
4
 
5
- Use it when you want an isolated second-pass review of a proposed change against the local checkout. The tool stays read-only, checks ticket fit and scope first, then looks for diff mismatches, correctness bugs, security/safety issues, unnecessary complexity, and missing validation.
6
-
7
- This package is adapted from the TLH code-reviewer workflow for use as a standalone pi extension.
5
+ Use it when you want an isolated second-pass review of a proposed change against the local checkout. The subagent uses read-only built-in tools plus any configured MCP tools, checks ticket fit and scope first, then looks for diff mismatches, correctness bugs, security/safety issues, unnecessary complexity, and missing validation.
8
6
 
9
7
  ## Install
10
8
 
@@ -46,7 +44,8 @@ When called, it launches an isolated in-memory child agent with:
46
44
  - an auto-selected review model that prefers contrarian-style opposite-provider and opposite-family candidates before same-provider fallbacks;
47
45
  - automatic model candidates constrained to the current Pi session's model scope when that scope is non-empty;
48
46
  - requested thinking taken from the active session when available, otherwise defaulting to `high` for reasoning models and `off` for non-reasoning models, then clamped to the selected model's supported level;
49
- - read-only tools only: `read`, `grep`, `find`, `ls`, and guarded `bash`;
47
+ - read-only built-in tools: `read`, `grep`, `find`, `ls`, and guarded `bash`;
48
+ - configured MCP tools (run without confirmation in the subagent; may have side effects; use for evidence gathering only; requires host Pi >=1.0.4; older hosts degrade to no MCP, no error); the global `mcp.json` always applies; a project `.pi/mcp.json` is loaded only when the host session has already trusted the project;
50
49
  - a local-checkout path guard for file inspection;
51
50
  - a bash guard that allows only direct read-only `git`, `gh`, or `pwd` invocations.
52
51
 
@@ -65,6 +64,7 @@ The model report is concise and includes a verdict, findings, validation notes,
65
64
 
66
65
  - The subagent is explicitly instructed not to implement changes.
67
66
  - Runtime guards block write/edit tools, shell control operators, pipelines, redirection, path traversal outside the checkout, mutating `git`/`gh` commands, `npm`/publish commands, and other filesystem mutation.
67
+ - Runtime guards apply to built-in tools only; configured MCP tools pass the guard, run without confirmation, and are limited only by prompt guidance.
68
68
  - Built-in file-inspection tools are preferred over shell commands for local files.
69
69
 
70
70
  ## Example