@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
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
|
|
10
|
-
- [`contrarian`](./extensions/contrarian): Adds a read-only
|
|
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`,
|
|
12
|
-
- [`oracle`](./extensions/oracle): Adds an Amp-style read-only
|
|
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):
|
|
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
|
|
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
|
|
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
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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)
|
|
1585
|
+
if (ctx.signal?.aborted) abortFromCaller();
|
|
1370
1586
|
if (ctx.signal && !ctx.signal.aborted) {
|
|
1371
|
-
ctx.signal.addEventListener("abort",
|
|
1587
|
+
ctx.signal.addEventListener("abort", abortFromCaller);
|
|
1372
1588
|
abortListenerAdded = true;
|
|
1373
1589
|
}
|
|
1374
1590
|
|
|
1375
1591
|
try {
|
|
1376
1592
|
emit();
|
|
1377
|
-
const
|
|
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 (!
|
|
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
|
-
|
|
1470
|
-
|
|
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 (
|
|
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
|
|
1501
|
-
const message =
|
|
1502
|
-
details.status =
|
|
1503
|
-
details.error =
|
|
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",
|
|
1736
|
+
if (ctx.signal && abortListenerAdded) ctx.signal.removeEventListener("abort", abortFromCaller);
|
|
1512
1737
|
unsubscribe?.();
|
|
1513
|
-
session
|
|
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,
|
|
@@ -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.
|
|
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.
|
|
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
|
|
14
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|