/** * Token Phish Blocker โ€” always-on anti-hedging policy for the Pi coding agent. * * Three responsibilities, one file: * 1. Inject the 4-rule policy into the system prompt on every turn * (before_agent_start). The policy ships inside this file โ€” nothing to * install, nothing that can go missing. A user override at * ~/.pi/agent/token-phish-blocker-policy.md replaces it verbatim. * 2. Register the token_phish_blocker_log tool the policy instructs the * model to call for every auto-fix. Each call appends a line to * ~/.pi/agent/token-phish-blocker.log โ€” a durable audit trail. * 3. Render a live ๐ŸŽฃ TPB ยท N fixes counter in the footer, so the policy * being "on" is directly observable, not just asserted. Set * TPB_NO_STATUS=1 to hide the counter (policy and logging unaffected). * * Every side effect is best-effort: disk or UI failure must never break the * session. The system-prompt injection is the only load-bearing path. */ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; import { Type } from "typebox"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; // Bun's os.homedir() reads the OS user-info lookup once at process startup and // ignores later `process.env.HOME` reassignment (unlike Node, which re-reads // HOME on every call). Checking process.env.HOME first keeps this working // under both runtimes and lets HOME-sandboxed tests (see test/simulate.ts) // actually redirect the extension's file paths. const AGENT_DIR = join(process.env.HOME || homedir(), ".pi", "agent"); const LOG_PATH = join(AGENT_DIR, "token-phish-blocker.log"); const POLICY_OVERRIDE_PATH = join(AGENT_DIR, "token-phish-blocker-policy.md"); const STATUS_KEY = "token-phish-blocker"; const BUILT_IN_POLICY = `## Token Phish Blocker โ€” always-on communication & auto-fix policy This section is not a skill and not optional guidance you choose to apply. It is injected into every session by the token-phish-blocker extension and governs how you communicate and act, regardless of project or task. **1. No hedging.** Never phrase a fixable gap as commentary instead of fixing it. Banned patterns (paraphrases count too): "one thing I didn't do", "one caveat", "it's worth noting that I didn't...", "just a heads up", "however, I didn't get to...", "unfortunately I wasn't able to...". If you catch yourself about to write a sentence like that, stop and go implement the fix instead of writing the sentence. **2. Auto-implement trivial and blocking fixes.** If something you notice while working is trivial to fix, or blocks the current task from actually working end to end, implement the fix yourself before you present the task as done. Do not ask permission first. Do not describe the fix as a suggestion or an option โ€” just make the change. **3. Stay in scope.** This authorizes fixes inside the blast radius of the current task (the file/feature already being touched, or something that would break the very thing you're delivering) โ€” not unrelated refactors or speculative scope creep. Anything genuinely ambiguous, architecturally significant, or outside that radius still gets surfaced to the user โ€” plainly, in one direct sentence, without the banned hedging phrasing from rule 1. **4. Log every auto-fix.** Every time you take the rule-2 path โ€” implementing something you would previously have just mentioned as a caveat โ€” call the \`token_phish_blocker_log\` tool with a one-line description, then continue. This call is mandatory for that specific pattern; it is the audit trail that proves this policy is actually operating, and it drives a live counter in the status line. Do not call it for ordinary planned implementation work โ€” only for the "I would have surfaced this as a limitation but I'm fixing it instead" pattern.`; function loadPolicy(): string { try { if (existsSync(POLICY_OVERRIDE_PATH)) { const custom = readFileSync(POLICY_OVERRIDE_PATH, "utf8").trim(); if (custom.length > 0) return custom; } } catch { // unreadable override falls back to the built-in policy } return BUILT_IN_POLICY; } type StatusCtx = { hasUI?: boolean; ui?: { setStatus?: (key: string, text: string) => void } }; export default function tokenPhishBlocker(pi: ExtensionAPI) { let count = 0; const render = () => `\u{1F3A3} TPB \u00b7 ${count} fix${count === 1 ? "" : "es"}`; const setStatus = (ctx: StatusCtx | undefined) => { try { const optOut = process.env.TPB_NO_STATUS; if (optOut && optOut !== "0") return; if (!ctx || ctx.hasUI === false) return; ctx.ui?.setStatus?.(STATUS_KEY, render()); } catch { // status-line visibility must never break the session } }; pi.on("session_start", (_event, ctx) => setStatus(ctx)); pi.on("before_agent_start", (event) => ({ systemPrompt: `${event.systemPrompt}\n\n${loadPolicy()}`, })); pi.registerTool({ name: "token_phish_blocker_log", label: "Token Phish Blocker", description: "Call this every time you implement a fix in place of surfacing it as a caveat/limitation, per the Token Phish Blocker policy in your system prompt. Do not call it for ordinary planned implementation work.", parameters: Type.Object({ description: Type.String({ description: "One-line description of what was auto-fixed and why it was in-scope", }), }), async execute(_toolCallId, params, _signal, _onUpdate, ctx) { count += 1; try { mkdirSync(dirname(LOG_PATH), { recursive: true }); appendFileSync(LOG_PATH, `${new Date().toISOString()}\t${ctx.cwd}\t${params.description}\n`); } catch { // best-effort audit log; never block the agent loop on disk failure } setStatus(ctx as StatusCtx); return { content: [{ type: "text", text: `Logged fix #${count}.` }], details: { count }, }; }, }); }