diff options
| author | user <user@clank> | 2026-07-23 20:17:18 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-12 19:34:35 +0200 |
| commit | 7cc3981b6f00eb157211e24244c223913890f435 (patch) | |
| tree | 06b2a1fec6c8c71a4e0a445c6156a0b506de8b4e | |
| parent | installers: exclude experimental sixel-images.ts from global + dev load (diff) | |
| download | sl0ppi-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.ts | 454 |
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); - } - }, - }); -} |
