@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
@@ -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 { AgentSession, ExtensionAPI, ExtensionFactory } from "@earendil-works/pi-coding-agent";
5
8
  import {
@@ -9,6 +12,7 @@ import {
9
12
  createAgentSession,
10
13
  getAgentDir,
11
14
  } from "@earendil-works/pi-coding-agent";
15
+ import * as piCodingAgent from "@earendil-works/pi-coding-agent";
12
16
  import { Type } from "typebox";
13
17
 
14
18
  import type { Usage } from "@earendil-works/pi-ai";
@@ -72,6 +76,33 @@ const CODE_REVIEWER_MODEL_PREFERENCES = [
72
76
  "grok-4.5",
73
77
  "gemini-3.5-flash",
74
78
  ];
79
+ const AZURE_MODEL_PREFERENCES = [
80
+ "gpt-6-astra ",
81
+ "gpt-6-astra",
82
+ "gpt-6.1-sol ",
83
+ "gpt-6-sol ",
84
+ "gpt-6-sol",
85
+ "gpt-5.6-sol",
86
+ "gpt-5.6-terra",
87
+ "gpt-6-luna ",
88
+ "gpt-6-luna",
89
+ "gpt-5.6-luna",
90
+ "gpt-5.5-pro",
91
+ "gpt-5.5",
92
+ "gpt-5.4-pro",
93
+ "gpt-5.4",
94
+ "gpt-5.3-codex",
95
+ "gpt-5-pro",
96
+ "gpt-5-chat-latest",
97
+ "gpt-5.2-pro",
98
+ "gpt-5.2",
99
+ "gpt-5.1",
100
+ "o3-pro",
101
+ "o1-pro",
102
+ "gpt-5.4-mini",
103
+ "gpt-5-mini",
104
+ ];
105
+
75
106
  const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
76
107
  "amazon-bedrock": [
77
108
  "global.anthropic.claude-opus-5-5 ",
@@ -102,39 +133,12 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
102
133
  "claude-sonnet-4-6",
103
134
  "claude-sonnet-4-5",
104
135
  ],
105
- "azure-openai-responses": [
106
- "gpt-6-astra ",
107
- "gpt-6-astra",
108
- "gpt-6.1-sol ",
109
- "gpt-6-sol ",
110
- "gpt-6-sol",
111
- "gpt-5.6-sol",
112
- "gpt-5.6-terra",
113
- "gpt-6-luna ",
114
- "gpt-6-luna",
115
- "gpt-5.6-luna",
116
- "gpt-5.5-pro",
117
- "gpt-5.5",
118
- "gpt-5.4-pro",
119
- "gpt-5.4",
120
- "gpt-5.3-codex",
121
- "gpt-5-pro",
122
- "gpt-5-chat-latest",
123
- "gpt-5.2-pro",
124
- "gpt-5.2",
125
- "gpt-5.1",
126
- "o3-pro",
127
- "o1-pro",
128
- "gpt-5.4-mini",
129
- "gpt-5-mini",
130
- ],
136
+ azure: AZURE_MODEL_PREFERENCES,
137
+ // Legacy alias for Pi 1.0.0–1.0.2 hosts (renamed to `azure` in Pi 1.0.3).
138
+ "azure-openai-responses": AZURE_MODEL_PREFERENCES,
131
139
  "cloudflare-ai-gateway": [
132
140
  "claude-fable-5",
133
141
  "claude-opus-5",
134
- "claude-opus-4.8",
135
- "claude-opus-4.7",
136
- "claude-opus-4.6",
137
- "claude-opus-4.5",
138
142
  "gpt-6-astra ",
139
143
  "gpt-6-astra",
140
144
  "gpt-5.5",
@@ -286,6 +290,92 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
286
290
  ],
287
291
  };
288
292
 
293
+ const SUBAGENT_MCP_TOOLS = [
294
+ "mcp__*",
295
+ "tool_search",
296
+ "list_mcp_resources",
297
+ "list_mcp_resource_templates",
298
+ "read_mcp_resource",
299
+ ];
300
+
301
+ // Derive project-trust for the isolated subagent session from the host tool context.
302
+ // Fails closed: returns false if the host has not trusted the project, or if the subagent
303
+ // runs in a different directory (e.g. a temp workspace), preventing an untrusted project's
304
+ // .pi/mcp.json from being loaded by the MCP extension.
305
+ function resolveSubagentProjectTrusted(
306
+ ctx: { cwd: string; isProjectTrusted(): boolean },
307
+ subagentCwd: string,
308
+ ): boolean {
309
+ // Feature-detected for older Pi hosts that may lack isProjectTrusted.
310
+ const hostTrusted =
311
+ typeof (ctx as { isProjectTrusted?: unknown }).isProjectTrusted === "function"
312
+ ? ctx.isProjectTrusted()
313
+ : false;
314
+ if (!hostTrusted) return false;
315
+ try {
316
+ return realpathSync(subagentCwd) === realpathSync(ctx.cwd);
317
+ } catch {
318
+ return false; // fail closed if realpath resolution fails
319
+ }
320
+ }
321
+
322
+ function isSubagentMcpTool(toolName: string): boolean {
323
+ return (
324
+ toolName.startsWith("mcp__") ||
325
+ toolName === "tool_search" ||
326
+ toolName === "list_mcp_resources" ||
327
+ toolName === "list_mcp_resource_templates" ||
328
+ toolName === "read_mcp_resource"
329
+ );
330
+ }
331
+
332
+ /** Returns true when v is a semver string >=1.0.4 (ignores prerelease; missing/garbage -> false). */
333
+ function isMcpCompatibleVersion(v: string | undefined): boolean {
334
+ if (!v) return false;
335
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
336
+ if (!m) return false;
337
+ const major = Number(m[1]),
338
+ minor = Number(m[2]),
339
+ patch = Number(m[3]);
340
+ if (major !== 1) return major > 1;
341
+ if (minor !== 0) return minor > 0;
342
+ return patch >= 4;
343
+ }
344
+
345
+ // Gated on VERSION >=1.0.4: on older hosts mcp__* allowlist entries are exact-name
346
+ // matches that resolve nothing, so spawning MCP servers provides no benefit.
347
+ function createMcpSubagentFactories(): ExtensionFactory[] {
348
+ const ns = piCodingAgent as Record<string, unknown>;
349
+ if (!isMcpCompatibleVersion(ns["VERSION"] as string | undefined)) return [];
350
+ const factories: ExtensionFactory[] = [];
351
+ const mcp = ns["createMcpExtension"];
352
+ const toolSearch = ns["createToolSearchExtension"];
353
+ if (typeof mcp === "function") factories.push((mcp as () => ExtensionFactory)());
354
+ if (typeof toolSearch === "function") factories.push((toolSearch as () => ExtensionFactory)());
355
+ return factories;
356
+ }
357
+
358
+ // Emit session_shutdown before dispose so MCP closes server connections (no leaked child processes).
359
+ async function disposeSubagentSession(session: AgentSession | undefined): Promise<void> {
360
+ if (!session) return;
361
+ try {
362
+ const runner = (
363
+ session as unknown as {
364
+ extensionRunner?: {
365
+ hasHandlers(n: string): boolean;
366
+ emit(e: { type: string; reason: string }): Promise<unknown>;
367
+ };
368
+ }
369
+ ).extensionRunner;
370
+ if (runner?.hasHandlers("session_shutdown")) {
371
+ await runner.emit({ type: "session_shutdown", reason: "quit" });
372
+ }
373
+ } catch {
374
+ // Shutdown errors must not prevent dispose.
375
+ }
376
+ session.dispose();
377
+ }
378
+
289
379
  const READ_ONLY_TOOL_NAMES = new Set(["read", "grep", "find", "ls", "bash"]);
290
380
  const READ_ONLY_BASH_COMMANDS = new Set(["gh", "git", "pwd"]);
291
381
  const READ_ONLY_GIT_SUBCOMMANDS = new Set([
@@ -321,7 +411,12 @@ const SAFE_GIT_BRANCH_FLAGS = new Set([
321
411
  "--no-merged",
322
412
  ]);
323
413
  const SAFE_GIT_GLOBAL_FLAGS = new Set(["--no-pager", "--no-optional-locks"]);
324
- const SAFE_GIT_CONFIG_OVERRIDES = ["core.pager=cat", "core.fsmonitor=false", "diff.external="];
414
+ const SAFE_GIT_CONFIG_OVERRIDES = [
415
+ "core.pager=cat",
416
+ "core.fsmonitor=false",
417
+ "diff.external=",
418
+ "log.showSignature=false",
419
+ ];
325
420
  const GIT_SUBCOMMAND_SAFE_FLAGS: Partial<Record<string, string[]>> = {
326
421
  blame: ["--no-textconv"],
327
422
  diff: ["--no-ext-diff", "--no-textconv"],
@@ -330,12 +425,6 @@ const GIT_SUBCOMMAND_SAFE_FLAGS: Partial<Record<string, string[]>> = {
330
425
  whatchanged: ["--no-ext-diff", "--no-textconv"],
331
426
  };
332
427
  const GIT_OPTIONS_REQUIRING_LOCAL_FILE = [
333
- {
334
- flag: "--contents",
335
- blockedPrefixes: ["--con"],
336
- reason:
337
- "Code Reviewer bash blocks git --contents because it can read local files outside built-in path guards.",
338
- },
339
428
  {
340
429
  flag: "--pathspec-from-file",
341
430
  blockedPrefixes: ["--pathspec"],
@@ -353,6 +442,15 @@ const GIT_SUBCOMMAND_OPTIONS_REQUIRING_LOCAL_FILE: Partial<
353
442
  Record<string, Array<{ reason: string; matches: (token: string) => boolean }>>
354
443
  > = {
355
444
  blame: [
445
+ {
446
+ reason:
447
+ "Code Reviewer bash blocks git blame --contents because it can read local files outside built-in path guards.",
448
+ matches: (token) => {
449
+ if (!token.startsWith("--")) return false;
450
+ const option = token.split("=", 1)[0]?.toLowerCase() ?? "";
451
+ return option === "--contents" || option.startsWith("--con");
452
+ },
453
+ },
356
454
  {
357
455
  reason:
358
456
  "Code Reviewer bash blocks git blame -S because it can read local revs files outside built-in path guards.",
@@ -859,6 +957,82 @@ function isInside(parent: string, child: string): boolean {
859
957
  );
860
958
  }
861
959
 
960
+ const UNICODE_SPACES_PATTERN = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
961
+
962
+ /**
963
+ * Mirrors normalizeWindowsShellPath from dist/utils/paths.js. No-op on
964
+ * non-win32 paths.
965
+ */
966
+ function normalizeWindowsShellPathLocal(filePath: string): string {
967
+ if (!filePath.startsWith("/") || filePath.startsWith("//") || filePath.includes("\\"))
968
+ return filePath;
969
+ const match = filePath.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
970
+ if (!match) return filePath;
971
+ const suffix = match[2]?.replaceAll("/", "\\");
972
+ return `${match[1].toUpperCase()}:\\${suffix ?? ""}`;
973
+ }
974
+
975
+ /**
976
+ * Mirrors Pi's normalizePath (dist/utils/paths.js). On win32, Git Bash / WSL
977
+ * drive-path normalisation always runs; only tilde expansion is gated by
978
+ * expandTilde.
979
+ */
980
+ function normalizePath(
981
+ input: string,
982
+ opts: { normalizeUnicodeSpaces?: boolean; stripAtPrefix?: boolean; expandTilde?: boolean } = {},
983
+ ): string {
984
+ const { normalizeUnicodeSpaces = false, stripAtPrefix = false, expandTilde = true } = opts;
985
+ let p = input;
986
+ if (normalizeUnicodeSpaces) p = p.replace(UNICODE_SPACES_PATTERN, " ");
987
+ if (stripAtPrefix && p.startsWith("@")) p = p.slice(1);
988
+ if (process.platform === "win32") p = normalizeWindowsShellPathLocal(p);
989
+ if (expandTilde) {
990
+ const home = os.homedir();
991
+ if (p === "~") p = home;
992
+ else if (p.startsWith("~/") || (process.platform === "win32" && p.startsWith("~\\")))
993
+ p = path.join(home, p.slice(2));
994
+ }
995
+ if (/^file:\/\//.test(p)) p = fileURLToPath(p);
996
+ return p;
997
+ }
998
+
999
+ /**
1000
+ * Mirrors Pi's resolveToCwd (dist/core/tools/path-utils.js). Exposed via
1001
+ * __test__ for parity tests; not imported by other extensions.
1002
+ */
1003
+ function resolveToCwd(rawPath: string, cwd: string): string {
1004
+ const normalized = normalizePath(rawPath, { normalizeUnicodeSpaces: true, stripAtPrefix: true });
1005
+ const base = normalizePath(cwd);
1006
+ return path.isAbsolute(normalized) ? path.resolve(normalized) : path.resolve(base, normalized);
1007
+ }
1008
+
1009
+ /** Mirrors tryMacOSScreenshotPath from dist/core/tools/path-utils.js */
1010
+ function tryMacOSScreenshotPathVariant(filePath: string): string {
1011
+ return filePath.replace(/ (AM|PM)\./gi, "\u202F$1.");
1012
+ }
1013
+ /** Mirrors tryNFDVariant from dist/core/tools/path-utils.js */
1014
+ function tryNFDPathVariant(filePath: string): string {
1015
+ return filePath.normalize("NFD");
1016
+ }
1017
+ /** Mirrors tryCurlyQuoteVariant from dist/core/tools/path-utils.js */
1018
+ function tryCurlyQuotePathVariant(filePath: string): string {
1019
+ return filePath.replace(/'/g, "\u2019");
1020
+ }
1021
+ /**
1022
+ * Returns all read path candidates including Pi's resolveReadPathAsync fallback
1023
+ * variants. Used only for the read tool guard; grep/find/ls use resolveToCwd only.
1024
+ */
1025
+ function getReadPathCandidates(resolved: string): string[] {
1026
+ const nfd = tryNFDPathVariant(resolved);
1027
+ return [
1028
+ resolved,
1029
+ tryMacOSScreenshotPathVariant(resolved),
1030
+ nfd,
1031
+ tryCurlyQuotePathVariant(resolved),
1032
+ tryCurlyQuotePathVariant(nfd),
1033
+ ];
1034
+ }
1035
+
862
1036
  function resolveToolPath(cwd: string, rawPath: string | undefined): string {
863
1037
  const input = rawPath?.trim() || ".";
864
1038
  const normalized = input.startsWith("@") ? input.slice(1) : input;
@@ -880,10 +1054,33 @@ async function assertToolPathInsideCwd(
880
1054
  return `${toolName} path must not traverse outside the local checkout.`;
881
1055
  }
882
1056
  }
1057
+ // A path is allowed if it resolves inside EITHER the realpath'd root (root) OR
1058
+ // the lexical cwd (lexicalCwd). This handles missing files in symlinked workspaces
1059
+ // where realpath fails and the lexical fallback starts with the alias prefix.
883
1060
  const root = await fs.realpath(cwd).catch(() => path.resolve(cwd));
1061
+ const lexicalCwd = path.resolve(cwd);
884
1062
  const resolved = resolveToolPath(cwd, rawPath);
885
1063
  const realPath = await fs.realpath(resolved).catch(() => resolved);
886
- if (!isInside(root, realPath)) return `${toolName} is limited to the local checkout: ${realPath}`;
1064
+ if (!isInside(root, realPath) && !isInside(lexicalCwd, realPath))
1065
+ return `${toolName} is limited to the local checkout: ${realPath}`;
1066
+ // Pi-effective path check (monotonic: block if either resolution is outside).
1067
+ // For the read tool, also check Pi's resolveReadPathAsync fallback variants so
1068
+ // a missing file whose variant resolves outside is not silently allowed.
1069
+ try {
1070
+ const piResolved = resolveToCwd(typeof rawPath === "string" ? rawPath : ".", cwd);
1071
+ const candidates = toolName === "read" ? getReadPathCandidates(piResolved) : [piResolved];
1072
+ // Deliberate policy: all candidates are checked regardless of existence (not
1073
+ // Pi's existence-ordered selection) so the guard cannot be raced by creating
1074
+ // a variant between the hook and the read. Trade-off: a read is blocked when
1075
+ // an escaping variant symlink sits next to a safe primary file.
1076
+ for (const candidate of candidates) {
1077
+ const candidateRealPath = await fs.realpath(candidate).catch(() => candidate);
1078
+ if (!isInside(root, candidateRealPath) && !isInside(lexicalCwd, candidateRealPath))
1079
+ return `${toolName} is limited to the local checkout: ${candidateRealPath}`;
1080
+ }
1081
+ } catch {
1082
+ return `${toolName} path could not be resolved safely.`;
1083
+ }
887
1084
  return undefined;
888
1085
  }
889
1086
 
@@ -1428,10 +1625,10 @@ function createCodeReviewerRuntimeGuardExtension(options: {
1428
1625
  });
1429
1626
 
1430
1627
  pi.on("tool_call", async (event) => {
1431
- if (!READ_ONLY_TOOL_NAMES.has(event.toolName)) {
1628
+ if (!READ_ONLY_TOOL_NAMES.has(event.toolName) && !isSubagentMcpTool(event.toolName)) {
1432
1629
  return {
1433
1630
  block: true,
1434
- reason: `code_reviewer exposes read-only tools only; ${event.toolName} is not allowed.`,
1631
+ reason: `code_reviewer exposes read-only built-in tools and MCP tools; ${event.toolName} is not allowed.`,
1435
1632
  };
1436
1633
  }
1437
1634
 
@@ -1505,7 +1702,7 @@ function buildSystemPrompt(options: {
1505
1702
  maxTurns: number;
1506
1703
  maxRunSeconds: number;
1507
1704
  }): string {
1508
- return `You are Code Reviewer, an isolated read-only review agent running inside the code-reviewer package. Review the proposed change against the stated task and the local checkout, then return a concise evidence-based review report.\n\nWorking directory: ${options.cwd}\nTurn budget: ${options.maxTurns} turns total, including your final answer.\nWall-clock budget: ${options.maxRunSeconds} seconds.\n\nAvailable tools are intended to be read-only: read, grep, find, ls, and bash. Use the built-in read, grep, find, and ls tools for local file inspection. Use bash only for a single read-only git, gh, or pwd invocation, such as git status/diff/show/log/blame, gh pr view/diff/list/status/checks, gh api GET calls, or pwd. A runtime guard blocks write/edit tools, shell pipelines/control operators, redirection, mutating git/gh commands, npm/publish commands, path traversal outside the checkout, and other filesystem mutation.\n\nReview priorities, in order:\n1. Ticket fit: does the change actually satisfy the requested task and stay in scope?\n2. Diff accuracy: do the files and edits match what the task claims changed?\n3. Correctness: could the change fail, regress behavior, mishandle errors, or break edge cases?\n4. Security and safety: look for unsafe command use, path handling issues, secret exposure, injection risks, destructive behavior, and policy violations.\n5. Simplicity and maintainability: call out unnecessary complexity, unclear behavior, or avoidable duplication.\n6. Tests and validation: check whether meaningful verification exists and whether gaps matter for this task.\n\nNon-negotiable constraints:\n- Never implement changes. Never edit, write, move, delete, format, install, commit, checkout, reset, clean, push, publish, or mutate GitHub.\n- Treat the supplied task and diff/context as claims to verify, not as facts. Cite file paths, line ranges, git diff/status output, or supplied diff excerpts as evidence.\n- Prefer the built-in file tools over bash when local file inspection is enough.\n- If evidence is missing, stale, or contradictory, say so plainly.\n- Keep the final review concise and actionable. Report the most important findings first; do not pad with non-findings.\n\nOutput format, exact order:\n## Verdict\nOne short paragraph with the overall review outcome.\n\n## Findings\nUse bullets ordered by severity. For each finding include: severity ('blocker', 'major', or 'minor'), a short title, evidence, and why it matters. If there are no findings, write '- none'.\n\n## Validation\nList the read-only checks you performed, including key files inspected, notable git/gh commands, and any uncertainty that limited the review, or '- none'.\n\n## Scope check\nState whether the change appears to match the requested task and note any missing or extra scope.`;
1705
+ return `You are Code Reviewer, an isolated read-only review agent running inside the code-reviewer package. Review the proposed change against the stated task and the local checkout, then return a concise evidence-based review report.\n\nWorking directory: ${options.cwd}\nTurn budget: ${options.maxTurns} turns total, including your final answer.\nWall-clock budget: ${options.maxRunSeconds} seconds.\n\nAvailable tools are intended to be read-only: read, grep, find, ls, and bash. Use the built-in read, grep, find, and ls tools for local file inspection. Use bash only for a single read-only git, gh, or pwd invocation, such as git status/diff/show/log/blame, gh pr view/diff/list/status/checks, gh api GET calls, or pwd. A runtime guard blocks write/edit tools, shell pipelines/control operators, redirection, mutating git/gh commands, npm/publish commands, path traversal outside the checkout, and other filesystem mutation. 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.\n\nReview priorities, in order:\n1. Ticket fit: does the change actually satisfy the requested task and stay in scope?\n2. Diff accuracy: do the files and edits match what the task claims changed?\n3. Correctness: could the change fail, regress behavior, mishandle errors, or break edge cases?\n4. Security and safety: look for unsafe command use, path handling issues, secret exposure, injection risks, destructive behavior, and policy violations.\n5. Simplicity and maintainability: call out unnecessary complexity, unclear behavior, or avoidable duplication.\n6. Tests and validation: check whether meaningful verification exists and whether gaps matter for this task.\n\nNon-negotiable constraints:\n- Never implement changes. Never edit, write, move, delete, format, install, commit, checkout, reset, clean, push, publish, or mutate GitHub.\n- Treat the supplied task and diff/context as claims to verify, not as facts. Cite file paths, line ranges, git diff/status output, or supplied diff excerpts as evidence.\n- Prefer the built-in file tools over bash when local file inspection is enough.\n- If evidence is missing, stale, or contradictory, say so plainly.\n- Keep the final review concise and actionable. Report the most important findings first; do not pad with non-findings.\n\nOutput format, exact order:\n## Verdict\nOne short paragraph with the overall review outcome.\n\n## Findings\nUse bullets ordered by severity. For each finding include: severity ('blocker', 'major', or 'minor'), a short title, evidence, and why it matters. If there are no findings, write '- none'.\n\n## Validation\nList the read-only checks you performed, including key files inspected, notable git/gh commands, and any uncertainty that limited the review, or '- none'.\n\n## Scope check\nState whether the change appears to match the requested task and note any missing or extra scope.`;
1509
1706
  }
1510
1707
 
1511
1708
  function buildUserPrompt(
@@ -1564,17 +1761,20 @@ function appendRunDetails(report: string, details: ReviewDetails): string {
1564
1761
  : "thinking unknown";
1565
1762
  const suffix = `\n\n---\nRun details: ${modelSummary}; ${thinkingSummary}; ${details.turns} turn(s); ${details.toolCalls.length} tool call(s); duration ${duration}; cwd ${details.cwd}; tools ${toolSummary}.`;
1566
1763
  if (!trimmed)
1567
- return `## Verdict\nNo review output was produced.\n\n## Findings\n- major — Missing review output. Evidence: the isolated code_reviewer session returned no assistant text. Why it matters: the review could not be completed.\n\n## Validation\n- none\n\n## Scope check\nReview could not be completed.\n\n## Run details\n- ${suffix.trim()}`;
1764
+ return `## Verdict\nNo review output was produced.\n\n## Findings\n- major — Missing review output. Evidence: the isolated code_reviewer session returned no assistant text. Why it matters: the review could not be completed.\n\n## Validation\n- none\n\n## Scope check\nReview could not be completed.${suffix}`;
1568
1765
  return `${trimmed}${suffix}`;
1569
1766
  }
1570
1767
 
1571
1768
  export const __test__ = {
1572
- assertToolPathInsideCwd,
1573
- buildSystemPrompt,
1574
- buildUserPrompt,
1769
+ SUBAGENT_MCP_TOOLS,
1770
+ isMcpCompatibleVersion,
1771
+ resolveSubagentProjectTrusted,
1772
+ createMcpSubagentFactories,
1773
+ disposeSubagentSession,
1575
1774
  createCodeReviewerRuntimeGuardExtension,
1775
+ assertToolPathInsideCwd,
1776
+ resolveToCwd,
1576
1777
  buildSafeGitCommand,
1577
- formatToolCall,
1578
1778
  getBlockedBashReason,
1579
1779
  isModelAvailabilityError,
1580
1780
  normalizeThinkingLevel,
@@ -1599,11 +1799,11 @@ export default function codeReviewerExtension(pi: ExtensionAPI) {
1599
1799
  name: "code_reviewer",
1600
1800
  label: "Code reviewer",
1601
1801
  description:
1602
- "Read-only isolated reviewer that checks a proposed change for ticket fit, diff accuracy, correctness, security, simplicity, and validation gaps without implementing anything.",
1802
+ "Isolated reviewer with read-only built-in tools and configured MCP tools that checks a proposed change for ticket fit, diff accuracy, correctness, security, simplicity, and validation gaps without implementing anything.",
1603
1803
  promptSnippet:
1604
- "Run an isolated read-only code review against the local checkout and optional diff; returns concise findings with evidence and run details.",
1804
+ "Run an isolated code review against the local checkout and optional diff using read-only built-in tools plus any configured MCP tools; returns concise findings with evidence and run details.",
1605
1805
  promptGuidelines: [
1606
- "Use code_reviewer when you want an independent read-only review of a proposed change or task implementation.",
1806
+ "Use code_reviewer when you want an independent review of a proposed change or task implementation; it uses read-only built-in tools and any configured MCP tools.",
1607
1807
  "Provide the requested task and any relevant diff/context. Do not use code_reviewer to implement or edit files.",
1608
1808
  ],
1609
1809
  parameters: CodeReviewerParams,
@@ -1661,7 +1861,8 @@ export default function codeReviewerExtension(pi: ExtensionAPI) {
1661
1861
  try {
1662
1862
  emit();
1663
1863
 
1664
- const isolatedSettingsManager = SettingsManager.inMemory({});
1864
+ const projectTrusted = resolveSubagentProjectTrusted(ctx, cwd);
1865
+ const isolatedSettingsManager = SettingsManager.inMemory({}, { projectTrusted });
1665
1866
  const resourceLoader = new DefaultResourceLoader({
1666
1867
  cwd,
1667
1868
  agentDir: getAgentDir(),
@@ -1673,6 +1874,7 @@ export default function codeReviewerExtension(pi: ExtensionAPI) {
1673
1874
  noContextFiles: true,
1674
1875
  extensionFactories: [
1675
1876
  createCodeReviewerRuntimeGuardExtension({ cwd, maxTurns: MAX_TURNS }),
1877
+ ...createMcpSubagentFactories(),
1676
1878
  ],
1677
1879
  systemPromptOverride: () =>
1678
1880
  buildSystemPrompt({
@@ -1712,10 +1914,12 @@ export default function codeReviewerExtension(pi: ExtensionAPI) {
1712
1914
  sessionManager: SessionManager.inMemory(cwd),
1713
1915
  model: candidate as CreateAgentSessionModel,
1714
1916
  thinkingLevel: thinking.effective,
1715
- tools: ["read", "grep", "find", "ls", "bash"],
1917
+ tools: ["read", "grep", "find", "ls", "bash", ...SUBAGENT_MCP_TOOLS],
1716
1918
  });
1717
1919
  attemptSession = created.session;
1718
1920
  session = attemptSession;
1921
+ // Connect extensions (MCP servers) before prompting. Feature-detected: Pi >=0.99.0.
1922
+ await (created.session as any).bindExtensions?.({ mode: "json" });
1719
1923
  attemptUnsubscribe = attemptSession.subscribe((event) => {
1720
1924
  addSessionEventUsage(usage, event);
1721
1925
  switch (event.type) {
@@ -1797,7 +2001,7 @@ export default function codeReviewerExtension(pi: ExtensionAPI) {
1797
2001
  if (attemptSession && !succeeded) {
1798
2002
  attemptUnsubscribe?.();
1799
2003
  if (unsubscribe === attemptUnsubscribe) unsubscribe = undefined;
1800
- attemptSession.dispose();
2004
+ await disposeSubagentSession(attemptSession);
1801
2005
  if (session === attemptSession) session = undefined;
1802
2006
  }
1803
2007
  }
@@ -1828,7 +2032,7 @@ export default function codeReviewerExtension(pi: ExtensionAPI) {
1828
2032
  details.error = failure.error;
1829
2033
  details.endedAt = Date.now();
1830
2034
  lastContent = appendRunDetails(
1831
- `## Verdict\nReview failed.\n\n## Findings\n- major — Review execution failed. Evidence: ${message}. Why it matters: the requested read-only review did not complete.\n\n## Validation\n- none\n\n## Scope check\nReview could not be completed.\n\n## Run details\n- failure: ${message}`,
2035
+ `## Verdict\nReview failed.\n\n## Findings\n- major — Review execution failed. Evidence: ${message}. Why it matters: the requested read-only review did not complete.\n\n## Validation\n- none\n\n## Scope check\nReview could not be completed.`,
1832
2036
  details,
1833
2037
  );
1834
2038
  emit();
@@ -1837,7 +2041,7 @@ export default function codeReviewerExtension(pi: ExtensionAPI) {
1837
2041
  if (runTimeout) clearTimeout(runTimeout);
1838
2042
  if (signal && abortListenerAdded) signal.removeEventListener("abort", abortFromCaller);
1839
2043
  unsubscribe?.();
1840
- session?.dispose();
2044
+ await disposeSubagentSession(session);
1841
2045
  }
1842
2046
  },
1843
2047
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-code-reviewer",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "A standalone pi extension that adds a read-only code_reviewer subagent tool for isolated code reviews.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -12,20 +12,14 @@ import type {
12
12
  } from "@earendil-works/pi-coding-agent";
13
13
 
14
14
  function hasUnsavedUserWork(entries: SessionEntry[]): boolean {
15
- let sawAssistant = false;
16
-
17
15
  for (let index = entries.length - 1; index >= 0; index -= 1) {
18
16
  const entry = entries[index];
19
17
  if (entry.type !== "message") continue;
20
- if (entry.message.role === "assistant") {
21
- sawAssistant = true;
22
- break;
23
- }
18
+ if (entry.message.role === "assistant") return false;
24
19
  if (entry.message.role === "user") return true;
25
20
  }
26
21
 
27
- if (sawAssistant) return false;
28
- return entries.some((entry) => entry.type === "message" && entry.message.role === "user");
22
+ return false;
29
23
  }
30
24
 
31
25
  export default function (pi: ExtensionAPI) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-confirm-destructive",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "A pi extension that confirms destructive session actions.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1,6 +1,6 @@
1
1
  # context-cap
2
2
 
3
- A pi extension that treats large-context models as having an effective 200k-token context window, so pi's built-in auto-compaction starts earlier, and avoids the dumb zone.
3
+ A pi extension that treats large-context models as having an effective 300k-token context window, so pi's built-in auto-compaction starts earlier, and avoids the dumb zone.
4
4
 
5
5
  By default, pi auto-compacts when:
6
6
 
@@ -11,10 +11,10 @@ contextTokens > model.contextWindow - reserveTokens
11
11
  This extension changes the active model's in-memory `contextWindow` to:
12
12
 
13
13
  ```text
14
- min(originalContextWindow, 200000)
14
+ min(originalContextWindow, 300000)
15
15
  ```
16
16
 
17
- With pi's default `reserveTokens` of 16,384, models larger than 200k will proactively compact around 183,616 tokens.
17
+ With pi's default `reserveTokens` of 16,384, models larger than 300k will proactively compact around 283,616 tokens.
18
18
 
19
19
  ## Commands
20
20
 
@@ -65,21 +65,21 @@ Then reload pi:
65
65
 
66
66
  ### Settings guard and alternative
67
67
 
68
- - On Pi hosts exposing `getSettings()`, the extension resolves `reserveTokens` using Pi's precedence: `compaction.modelOverrides["provider/model-id"].reserveTokens`, then `compaction.reserveTokens`, then the default `16,384`. It skips the cap when the resolved value is at least `200,000` (which would make the threshold zero or negative), or when any explicit entry exists for that model. Hosts without `getSettings()` use the older cap behavior without this guard.
69
- - Pi's provider request path also derives an output-token budget from the effective window (`contextWindow - contextTokens - 4096`, with any provider-specific clamp applied afterward). The cap can therefore reduce response or thinking-token budgets near the limit, in addition to causing earlier compaction; Pi's overflow handling may also treat a request above 200k as overflow and retry after compaction. Use `/context-cap off` when you need the full model window temporarily.
70
- - As a session-scoped alternative, configure Pi's own `compaction.modelOverrides` instead of this extension. The exact key is `provider/model-id`; to match this extension's default 200k threshold for a model with original window `W`, let `R` be the configured `reserveTokens` value (default `16,384`) and use:
68
+ - On Pi hosts exposing `getSettings()`, the extension skips the cap whenever `compaction.modelOverrides` contains the active model's key (`provider/model-id`), whatever fields that entry sets; it does not read `reserveTokens` from the override. Otherwise it uses `compaction.reserveTokens`, then the default `16,384`, and skips the cap when that value is at least `300,000` (which would make the threshold zero or negative). Hosts without `getSettings()` use the older cap behavior without this guard.
69
+ - Pi's provider request path also derives an output-token budget from the effective window (`contextWindow - contextTokens - 4096`, with any provider-specific clamp applied afterward). The cap can therefore reduce response or thinking-token budgets near the limit, in addition to causing earlier compaction; Pi's overflow handling may also treat a request above 300k as overflow and retry after compaction. Use `/context-cap off` when you need the full model window temporarily.
70
+ - As a session-scoped alternative, configure Pi's own `compaction.modelOverrides` instead of this extension. The exact key is `provider/model-id`; to match this extension's default 300k threshold for a model with original window `W`, let `R` be the configured `reserveTokens` value (default `16,384`) and use:
71
71
 
72
72
  ```text
73
- reserveTokens = W - 200000 + R
73
+ reserveTokens = W - 300000 + R
74
74
  ```
75
75
 
76
- For example, with the default `R`, a 1,000,000-token model uses `816384`, yielding the same `183616` threshold as `200000 - 16384`:
76
+ For example, with the default `R`, a 1,000,000-token model uses `716384`, yielding the same `283616` threshold as `300000 - 16384`:
77
77
 
78
78
  ```json
79
79
  {
80
80
  "compaction": {
81
81
  "modelOverrides": {
82
- "provider/model-id": { "reserveTokens": 816384 }
82
+ "provider/model-id": { "reserveTokens": 716384 }
83
83
  }
84
84
  }
85
85
  }
@@ -1,7 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { Model, Api } from "@earendil-works/pi-ai";
3
3
 
4
- const DEFAULT_MAX_CONTEXT_WINDOW = 200_000;
4
+ const DEFAULT_MAX_CONTEXT_WINDOW = 300_000;
5
5
  const DEFAULT_RESERVE_TOKENS = 16_384;
6
6
  const PROCESS_STATE_KEY = Symbol.for("pi-extensions.context-cap.model-holds.v1");
7
7
  type AnyModel = Model<Api> | Model<any>;
@@ -224,7 +224,7 @@ function unavailableReason(result: ApplyResult | undefined): string | undefined
224
224
  return "physical model has no compatible context-window limit";
225
225
  }
226
226
  if (result?.reason === "reserve") {
227
- return `compaction reserveTokens (${formatTokens(result.reserveTokens ?? DEFAULT_MAX_CONTEXT_WINDOW)}) is at least 200k; the auto-compaction threshold would be zero or negative`;
227
+ return `compaction reserveTokens (${formatTokens(result.reserveTokens ?? DEFAULT_MAX_CONTEXT_WINDOW)}) is at least 300k; the auto-compaction threshold would be zero or negative`;
228
228
  }
229
229
  if (result?.reason === "modelOverride") {
230
230
  return `compaction.modelOverrides[\"${result.key}\"] is configured; leaving the model window unchanged`;
@@ -317,7 +317,7 @@ export default function contextCapExtension(pi: ExtensionAPI) {
317
317
  }
318
318
 
319
319
  pi.registerCommand("context-cap", {
320
- description: "Toggle the 200k effective context-window cap for auto-compaction",
320
+ description: "Toggle the 300k effective context-window cap for auto-compaction",
321
321
  getArgumentCompletions: (prefix) => {
322
322
  const commands = ["on", "off", "toggle", "status"];
323
323
  const matches = commands.filter((command) => command.startsWith(prefix.trim()));
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-context-cap",
3
- "version": "0.1.13",
4
- "description": "A pi extension that caps effective model context windows at 200k tokens for earlier auto-compaction.",
3
+ "version": "0.1.15",
4
+ "description": "A pi extension that caps effective model context windows at 300k tokens for earlier auto-compaction.",
5
5
  "keywords": [
6
6
  "pi-package",
7
7
  "pi",
@@ -1,6 +1,6 @@
1
1
  # contrarian
2
2
 
3
- A read-only contrarian subagent for [pi](https://github.com/earendil-works/pi).
3
+ A contrarian subagent with read-only built-in tools and configured MCP tools for [pi](https://github.com/earendil-works/pi).
4
4
 
5
5
  It adds a `contrarian` tool that spins up a separate pi subprocess to stress-test plans, designs, assumptions, bug hypotheses, review conclusions, and product directions by steelmanning the strongest credible opposing case.
6
6
 
@@ -8,7 +8,7 @@ This is adapted from the TLH [`contrarian` subagent prompt](https://github.com/d
8
8
 
9
9
  ## What it does
10
10
 
11
- - creates an isolated read-only subprocess
11
+ - creates an isolated subprocess with read-only built-in tools (read, grep, find, ls) and any configured MCP tools
12
12
  - prefers a strong model on the opposite model family/provider when available, then falls back to the best available model
13
13
  - requests `high` reasoning by default for reasoning models, then clamps to the model-supported thinking level
14
14
  - defaults to `read,grep,find,ls`, with optional non-mutating bash inspection
@@ -81,7 +81,8 @@ When the current Pi session has a non-empty model scope, Contrarian limits autom
81
81
 
82
82
  ## Notes
83
83
 
84
- - The contrarian is intentionally **read-only**.
84
+ - The contrarian subprocess uses read-only built-in tools (read, grep, find, ls) by default, plus any configured MCP tools. It can optionally include bash for non-mutating inspection.
85
+ - Configured MCP tools run without confirmation inside the subprocess and may have side effects — use them only for gathering evidence. MCP access requires host Pi >=1.0.4; older hosts degrade to no MCP, no error.
85
86
  - It never implements fixes or produces patches.
86
87
  - Its output separates confirmed objections, plausible concerns, and unresolved unknowns.
87
88
  - It is best for adversarial review before committing to a plan, design, or conclusion.