aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-23 20:17:18 +0200
committeruser <user@clank>2026-08-12 19:34:35 +0200
commit7cc3981b6f00eb157211e24244c223913890f435 (patch)
tree06b2a1fec6c8c71a4e0a445c6156a0b506de8b4e
parentinstallers: exclude experimental sixel-images.ts from global + dev load (diff)
downloadsl0ppi-7cc3981b6f00eb157211e24244c223913890f435.tar.gz
sl0ppi-7cc3981b6f00eb157211e24244c223913890f435.tar.xz
sl0ppi-7cc3981b6f00eb157211e24244c223913890f435.zip
remove audit-fanout extension
Drop the parallel audit-fanout subagent extension from the repo and the global extensions dir.
-rw-r--r--pi-ext/audit-fanout.ts454
1 files changed, 0 insertions, 454 deletions
diff --git a/pi-ext/audit-fanout.ts b/pi-ext/audit-fanout.ts
deleted file mode 100644
index 3655c04..0000000
--- a/pi-ext/audit-fanout.ts
+++ /dev/null
@@ -1,454 +0,0 @@
-/**
- * Audit Fan-out
- *
- * A minimal, audit-focused alternative to a full subagent framework. It runs N
- * independent "auditor" child pi sessions in parallel, each with a FRESH context
- * (a new `pi --print` process = clean slate) and a single focused goal, then
- * collects their findings.
- *
- * Fresh context is the point: each auditor starts blank apart from its task,
- * reads the code itself, and reports independently — ideal for code/security
- * audits where you want separate eyes on separate concerns.
- *
- * Model: auditors INHERIT the parent session's current model + thinking level by
- * default (we want intelligent auditors). Override per-run with `model`.
- * Auth needs nothing special — children read ~/.pi/agent/auth.json directly.
- *
- * Children run read-only: `--exclude-tools edit,write` so they can read/grep/
- * find/ls/bash for recon but never mutate the repo. `-ne --no-session` keeps them
- * lean and ephemeral; context files (AGENTS.md) still load so they respect repo
- * conventions.
- *
- * LIVE FEEDBACK: each child is spawned with `--mode json`; we parse the event
- * stream incrementally to show, per auditor, what it's doing right now (reading
- * a file, grepping, composing findings) plus a ticking elapsed timer — so a slow
- * model (Opus first-token latency) never looks hung.
- *
- * Two entry points:
- * - tool `audit` — the parent model can call it mid-conversation and synthesize.
- * - command `/audit "focus one" "focus two" ...` — you drive it; results are
- * written to .pi-audit/<ts>/ and the parent is asked to synthesize.
- */
-
-import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
-import { Type } from "typebox";
-import { spawn } from "node:child_process";
-import { mkdirSync, writeFileSync } from "node:fs";
-import { join, basename } from "node:path";
-
-const PI_BIN = process.env.PI_BIN || "pi";
-const DEFAULT_CONCURRENCY = 6;
-const DEFAULT_TIMEOUT_MS = 900_000; // 15 min per auditor
-const MAX_REPORT_CHARS = 16_000; // cap per-auditor text in the tool return
-
-type Task = { focus: string; scope?: string };
-type AuditResult = { focus: string; ok: boolean; text: string; error?: string; file?: string };
-type Phase = "queued" | "running" | "done" | "failed";
-type ChildState = { focus: string; phase: Phase; activity: string; startedAt?: number; endedAt?: number };
-
-// ── Prompt ───────────────────────────────────────────────────────────────────
-
-function buildPrompt(task: Task): string {
- const scope = task.scope ? `\nScope (concentrate your reading here): ${task.scope}` : "";
- return [
- "You are a meticulous code / security auditor. AUDIT ONLY — do not modify any files.",
- `Audit focus: ${task.focus}${scope}`,
- "",
- "Method:",
- "- Use read / grep / find / ls / bash for recon only. Never edit or write.",
- "- Find concrete, real issues in scope: bugs, vulnerabilities, unsafe/untrusted",
- " input handling, auth/authz flaws, injection, race conditions, resource leaks,",
- " memory-safety issues, and correctness/perf footguns.",
- "- For each finding, report: [SEVERITY] path:line — what is wrong, why it matters,",
- " and a concrete suggested fix direction.",
- "- Cite exact file:line locations. No vague generalities. If the in-scope code",
- " genuinely looks clean, say so explicitly rather than inventing issues.",
- "",
- "Output a concise markdown findings list, ranked by severity (Critical / High /",
- "Medium / Low). Start with a one-line summary of what you audited.",
- ].join("\n");
-}
-
-// A short human label for what a tool call is doing, for the live status line.
-function shortArg(toolName: string, args: any): string {
- if (!args) return "";
- const p = args.path ?? args.file;
- if (p) return basename(String(p));
- if (args.pattern) return `"${String(args.pattern).slice(0, 24)}"`;
- if (args.command) return String(args.command).replace(/\s+/g, " ").trim().slice(0, 28);
- return "";
-}
-
-function assistantText(message: any): string {
- return (message?.content ?? [])
- .filter((b: any) => b?.type === "text")
- .map((b: any) => b.text)
- .join("")
- .trim();
-}
-
-// ── Child invocation (incremental JSON parsing for live activity) ─────────────
-
-function runChild(
- task: Task,
- opts: { model?: string; cwd: string; timeoutMs: number; signal?: AbortSignal },
- onActivity: (activity: string) => void,
-): Promise<AuditResult> {
- return new Promise((resolve) => {
- const args = ["--print", "--mode", "json", "--no-session", "-ne", "--exclude-tools", "edit,write"];
- if (opts.model) args.push("--model", opts.model);
- args.push(buildPrompt(task));
-
- let finalText = "";
- let stderr = "";
- let buf = "";
- let activity = "starting…";
- let composedChars = 0;
- const errors: string[] = [];
- let done = false;
-
- // stdin MUST be closed ("ignore" => /dev/null): `pi --print` with a non-TTY
- // open-pipe stdin blocks waiting for input, which hangs the child forever.
- const child = spawn(PI_BIN, args, {
- cwd: opts.cwd,
- env: process.env,
- stdio: ["ignore", "pipe", "pipe"],
- });
-
- const finish = (r: AuditResult) => {
- if (done) return;
- done = true;
- clearTimeout(timer);
- opts.signal?.removeEventListener("abort", onAbort);
- resolve(r);
- };
- const timer = setTimeout(() => {
- child.kill("SIGTERM");
- finish({ focus: task.focus, ok: false, text: "", error: `timed out after ${opts.timeoutMs}ms` });
- }, opts.timeoutMs);
- const onAbort = () => {
- child.kill("SIGTERM");
- finish({ focus: task.focus, ok: false, text: "", error: "aborted" });
- };
- opts.signal?.addEventListener("abort", onAbort, { once: true });
-
- const handleLine = (line: string) => {
- const s = line.trim();
- if (!s.startsWith("{")) return;
- let ev: any;
- try {
- ev = JSON.parse(s);
- } catch {
- return;
- }
- switch (ev?.type) {
- case "tool_execution_start": {
- const a = shortArg(ev.toolName, ev.args);
- activity = `⚙ ${ev.toolName}${a ? " " + a : ""}`;
- break;
- }
- case "message_start":
- if (ev.message?.role === "assistant") {
- activity = "✍ composing…";
- composedChars = 0;
- }
- break;
- case "message_update":
- if (ev.message?.role === "assistant") {
- const d = ev.assistantMessageEvent?.delta;
- if (typeof d === "string") {
- composedChars += d.length;
- activity = `✍ composing… ${composedChars}c`;
- }
- }
- break;
- case "message_end":
- if (ev.message?.role === "assistant") {
- const t = assistantText(ev.message);
- if (t) finalText = t;
- }
- break;
- case "error":
- errors.push(String(ev.error?.message ?? ev.message ?? "error"));
- break;
- }
- onActivity(activity);
- };
-
- child.stdout.on("data", (d) => {
- buf += d.toString();
- let i: number;
- while ((i = buf.indexOf("\n")) >= 0) {
- handleLine(buf.slice(0, i));
- buf = buf.slice(i + 1);
- }
- });
- child.stderr.on("data", (d) => (stderr += d.toString()));
- child.on("error", (e) =>
- finish({ focus: task.focus, ok: false, text: "", error: `spawn failed: ${String(e)}` }),
- );
- child.on("close", (code) => {
- if (buf.trim()) handleLine(buf);
- if (finalText) {
- finish({ focus: task.focus, ok: true, text: finalText });
- } else {
- const err =
- errors.join("; ") || stderr.trim().split("\n").slice(-3).join(" ") || `exited ${code}, no output`;
- finish({ focus: task.focus, ok: false, text: "", error: err });
- }
- });
- });
-}
-
-async function runPool<T, R>(items: T[], limit: number, worker: (item: T, i: number) => Promise<R>): Promise<R[]> {
- const results = new Array<R>(items.length);
- let next = 0;
- const n = Math.max(1, Math.min(limit, items.length));
- await Promise.all(
- Array.from({ length: n }, async () => {
- while (true) {
- const i = next++;
- if (i >= items.length) break;
- results[i] = await worker(items[i], i);
- }
- }),
- );
- return results;
-}
-
-// ── Model inheritance ────────────────────────────────────────────────────────
-
-function currentModelString(pi: ExtensionAPI, ctx: any): string | undefined {
- const m = ctx?.model;
- if (!m?.provider || !m?.id) return undefined;
- const base = `${m.provider}/${m.id}`;
- let level: string | undefined;
- try {
- level = pi.getThinkingLevel?.() as string | undefined;
- } catch {
- /* ignore */
- }
- return level && level !== "off" && level !== "none" ? `${base}:${level}` : base;
-}
-
-// ── Orchestration ────────────────────────────────────────────────────────────
-
-function slug(s: string): string {
- return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "audit";
-}
-
-async function runAudit(
- tasks: Task[],
- opts: {
- model?: string;
- concurrency: number;
- cwd: string;
- signal?: AbortSignal;
- onStatus?: (states: ChildState[]) => void;
- },
-): Promise<{ dir: string; results: AuditResult[] }> {
- const ts = new Date().toISOString().replace(/[:.]/g, "-");
- const dir = join(opts.cwd, ".pi-audit", ts);
- try {
- mkdirSync(dir, { recursive: true });
- } catch {
- /* best effort */
- }
-
- const states: ChildState[] = tasks.map((t) => ({ focus: t.focus, phase: "queued", activity: "" }));
- let lastEmit = 0;
- const emit = (force = false) => {
- const now = Date.now();
- if (!force && now - lastEmit < 200) return; // throttle event-driven emits
- lastEmit = now;
- opts.onStatus?.(states);
- };
- // Wall-clock ticker so the elapsed timer keeps moving even before the model
- // emits its first event (Opus first-token latency). 1fps footer update — cheap.
- const ticker = setInterval(() => emit(true), 1000);
- emit(true);
-
- const results = await runPool(tasks, opts.concurrency, async (task, i) => {
- states[i].phase = "running";
- states[i].startedAt = Date.now();
- states[i].activity = "starting…";
- emit(true);
-
- const r = await runChild(
- task,
- { model: opts.model, cwd: opts.cwd, timeoutMs: DEFAULT_TIMEOUT_MS, signal: opts.signal },
- (activity) => {
- states[i].activity = activity;
- emit();
- },
- );
-
- states[i].phase = r.ok ? "done" : "failed";
- states[i].endedAt = Date.now();
- states[i].activity = r.ok ? "done" : r.error ?? "failed";
- emit(true);
-
- const name = `${String(i + 1).padStart(2, "0")}-${slug(task.focus)}.md`;
- r.file = join(dir, name);
- const body = r.ok ? r.text : `> AUDIT FAILED: ${r.error ?? "unknown error"}`;
- try {
- writeFileSync(r.file, `# Audit: ${task.focus}\n\n${body}\n`);
- } catch {
- /* ignore */
- }
- return r;
- });
-
- clearInterval(ticker);
- emit(true);
-
- try {
- const combined = results
- .map((r, i) => `## Auditor ${i + 1}: ${r.focus} ${r.ok ? "" : "(FAILED)"}\n\n${r.ok ? r.text : `> ${r.error}`}`)
- .join("\n\n---\n\n");
- writeFileSync(join(dir, "report.md"), `# Audit report (${results.length} auditors)\n\n${combined}\n`);
- } catch {
- /* ignore */
- }
-
- return { dir, results };
-}
-
-// ── Status line rendering ────────────────────────────────────────────────────
-
-function clip(s: string, n: number): string {
- s = s.replace(/\s+/g, " ").trim();
- return s.length > n ? s.slice(0, n - 1) + "…" : s;
-}
-
-function phaseIcon(p: Phase): string {
- return p === "done" ? "✓" : p === "failed" ? "✗" : p === "running" ? "⟳" : "·";
-}
-
-function makeStatusRenderer(ctx: any) {
- return (states: ChildState[]) => {
- if (!ctx?.ui?.setStatus) return;
- states.forEach((s, i) => {
- const el = s.startedAt ? Math.round(((s.endedAt ?? Date.now()) - s.startedAt) / 1000) : 0;
- const line = `${phaseIcon(s.phase)} audit ${i + 1}/${states.length} ${clip(s.focus, 26)} · ${clip(
- s.activity || s.phase,
- 30,
- )} · ${el}s`;
- ctx.ui.setStatus(`audit:${i + 1}`, line);
- });
- };
-}
-
-function clearStatus(ctx: any, count: number) {
- if (!ctx?.ui?.setStatus) return;
- for (let i = 1; i <= count; i++) ctx.ui.setStatus(`audit:${i}`, undefined);
-}
-
-function formatForModel(dir: string, results: AuditResult[]): string {
- const ok = results.filter((r) => r.ok).length;
- const header = `Audit fan-out complete: ${ok}/${results.length} auditors succeeded. Raw reports: ${dir}`;
- const sections = results.map((r, i) => {
- const body = r.ok
- ? r.text.length > MAX_REPORT_CHARS
- ? r.text.slice(0, MAX_REPORT_CHARS) + `\n\n…[truncated; full report at ${r.file}]`
- : r.text
- : `AUDIT FAILED: ${r.error}`;
- return `## Auditor ${i + 1}: ${r.focus}\n\n${body}`;
- });
- return `${header}\n\n${sections.join("\n\n---\n\n")}`;
-}
-
-// ── Task parsing for the /audit command ──────────────────────────────────────
-
-function parseTasks(argStr: string): Task[] {
- const re = /"([^"]*)"|'([^']*)'/g;
- const found: string[] = [];
- let m: RegExpExecArray | null;
- while ((m = re.exec(argStr))) {
- const v = (m[1] ?? m[2]).trim();
- if (v) found.push(v);
- }
- if (found.length === 0) {
- const t = argStr.trim();
- return t ? [{ focus: t }] : [];
- }
- return found.map((focus) => ({ focus }));
-}
-
-// ── Registration ─────────────────────────────────────────────────────────────
-
-export default function (pi: ExtensionAPI) {
- pi.registerTool({
- name: "audit",
- label: "Audit fan-out",
- description:
- "Run several independent auditor subagents in parallel, each with a fresh context and a single focused goal (read-only recon). Returns their combined findings for you to synthesize. Use for code/security audits split across concerns.",
- promptSnippet: "Fan out parallel read-only auditor subagents over focused concerns and collect findings",
- parameters: Type.Object({
- tasks: Type.Array(
- Type.Object({
- focus: Type.String({ description: "What to audit / what to look for (one focused concern)" }),
- scope: Type.Optional(Type.String({ description: "Optional path or subsystem to concentrate on" })),
- }),
- { description: "Independent audit tasks; each runs as its own parallel fresh-context auditor", minItems: 1 },
- ),
- model: Type.Optional(
- Type.String({ description: "Model for auditors (default: inherit the current session model + thinking)" }),
- ),
- concurrency: Type.Optional(Type.Number({ description: `Max parallel auditors (default ${DEFAULT_CONCURRENCY})` })),
- }),
- async execute(_toolCallId: string, params: any, signal: AbortSignal, _onUpdate: any, ctx: any) {
- const tasks: Task[] = params.tasks;
- const model = params.model || currentModelString(pi, ctx);
- const concurrency = params.concurrency ?? DEFAULT_CONCURRENCY;
- const onStatus = makeStatusRenderer(ctx);
- try {
- const { dir, results } = await runAudit(tasks, {
- model,
- concurrency,
- cwd: ctx?.cwd ?? process.cwd(),
- signal,
- onStatus,
- });
- return {
- content: [{ type: "text", text: formatForModel(dir, results) }],
- details: { dir, model, count: results.length, ok: results.filter((r) => r.ok).length },
- };
- } finally {
- clearStatus(ctx, tasks.length);
- }
- },
- });
-
- pi.registerCommand("audit", {
- description: 'Fan out parallel read-only auditors: /audit "focus one" "focus two" ...',
- handler: async (args: string, ctx: any) => {
- const tasks = parseTasks(args);
- if (tasks.length === 0) {
- ctx?.ui?.notify?.('Usage: /audit "focus one" "focus two" …', "warning");
- return;
- }
- const model = currentModelString(pi, ctx);
- ctx?.ui?.notify?.(`Launching ${tasks.length} auditor(s) on ${model ?? "default model"}…`, "info");
- const onStatus = makeStatusRenderer(ctx);
- try {
- const { dir, results } = await runAudit(tasks, {
- model,
- concurrency: DEFAULT_CONCURRENCY,
- cwd: ctx?.cwd ?? process.cwd(),
- onStatus,
- });
- const ok = results.filter((r) => r.ok).length;
- ctx?.ui?.notify?.(`Audit done: ${ok}/${results.length} succeeded. Reports in ${dir}`, ok ? "info" : "warning");
- pi.sendUserMessage(
- `Audit fan-out finished: ${ok}/${results.length} auditors succeeded. Raw reports are in ${dir} ` +
- `(one .md per auditor, plus report.md). Read them and produce a synthesized, de-duplicated, ` +
- `severity-ranked findings report. Call out anything multiple auditors independently flagged.`,
- { deliverAs: "followUp" },
- );
- } finally {
- clearStatus(ctx, tasks.length);
- }
- },
- });
-}