@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.
- package/README.md +8 -8
- package/extensions/agent-workflow-audit/README.md +1 -1
- package/extensions/agent-workflow-audit/index.ts +277 -44
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/git.ts +0 -8
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/web/index.html +3 -3
- package/extensions/code-reviewer/README.md +5 -5
- package/extensions/code-reviewer/index.ts +258 -54
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/confirm-destructive/index.ts +2 -8
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/README.md +9 -9
- package/extensions/context-cap/index.ts +3 -3
- package/extensions/context-cap/package.json +2 -2
- package/extensions/contrarian/README.md +4 -3
- package/extensions/contrarian/index.ts +47 -40
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dirty-repo-guard/index.ts +0 -4
- package/extensions/dirty-repo-guard/package.json +1 -1
- package/extensions/dynamic-context-pruning/README.md +5 -5
- package/extensions/dynamic-context-pruning/index.ts +114 -232
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +2 -2
- package/extensions/fast/package.json +1 -1
- package/extensions/inline-bash/index.ts +0 -16
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/README.md +1 -1
- package/extensions/librarian/index.ts +247 -39
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/README.md +5 -5
- package/extensions/minimal-footer/index.ts +3 -10
- package/extensions/minimal-footer/minimal-footer.example.json +1 -1
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/oracle/README.md +4 -3
- package/extensions/oracle/index.ts +45 -38
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/README.md +3 -0
- package/extensions/permission-gate/index.ts +151 -28
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/project-mcp-json/README.md +2 -2
- package/extensions/project-mcp-json/package.json +1 -1
- package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
- package/extensions/quiet-tools/README.md +30 -19
- package/extensions/quiet-tools/index.ts +233 -166
- package/extensions/quiet-tools/package.json +2 -2
- package/extensions/todo/index.ts +0 -19
- package/extensions/todo/package.json +1 -1
- package/extensions/triage-comments/README.md +4 -2
- package/extensions/triage-comments/index.ts +349 -29
- package/extensions/triage-comments/package.json +1 -1
- 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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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 = [
|
|
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)
|
|
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
|
|
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
|
|
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
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2044
|
+
await disposeSubagentSession(session);
|
|
1841
2045
|
}
|
|
1842
2046
|
},
|
|
1843
2047
|
});
|
|
@@ -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
|
-
|
|
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
|
# context-cap
|
|
2
2
|
|
|
3
|
-
A pi extension that treats large-context models as having an effective
|
|
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,
|
|
14
|
+
min(originalContextWindow, 300000)
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
With pi's default `reserveTokens` of 16,384, models larger than
|
|
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
|
|
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
|
|
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
|
|
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 -
|
|
73
|
+
reserveTokens = W - 300000 + R
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
For example, with the default `R`, a 1,000,000-token model uses `
|
|
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":
|
|
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 =
|
|
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
|
|
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
|
|
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.
|
|
4
|
-
"description": "A pi extension that caps effective model context windows at
|
|
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
|
|
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
|
|
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
|
|
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.
|