pi-agent-supervisor 1.0.0

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/AGENTS.md ADDED
@@ -0,0 +1,56 @@
1
+ # Agent Supervisor — Agent Usage Guide
2
+
3
+ > You are an AI agent. The supervisor is ALWAYS watching. It blocks dangerous operations automatically. You cannot disable it.
4
+
5
+ ## What the Supervisor Does
6
+
7
+ The supervisor runs passively on every tool call you make. It:
8
+
9
+ 1. **Blocks dangerous commands** — `rm -rf /`, `git push --force`, `sudo`, fork bombs
10
+ 2. **Protects sensitive files** — can't write to `.env`, credentials, SSH keys
11
+ 3. **Enforces rate limits** — warns at 50 calls/min, blocks at 80
12
+ 4. **Tracks errors** — escalates to human after 3 consecutive errors
13
+ 5. **Records everything** — append-only audit log in `.supervisor/audit.log`
14
+
15
+ ## When You Get Blocked
16
+
17
+ If a command is blocked, you have two options:
18
+
19
+ 1. **Find an alternative** — use a safer approach
20
+ 2. **Request override** — call `supervisor_override(reason)` to ask the human
21
+
22
+ ```
23
+ → supervisor_override(reason="Need to force push the rebased feature branch")
24
+ ```
25
+
26
+ The human will see the request and can approve or deny.
27
+
28
+ ## Checking Status
29
+
30
+ ```
31
+ supervisor_status()
32
+ ```
33
+
34
+ Shows:
35
+ - Current tool call rate
36
+ - Error count and consecutive errors
37
+ - Number of blocked calls
38
+ - Audit log location
39
+
40
+ ## Reading the Audit Log
41
+
42
+ ```
43
+ supervisor_log(tail=20)
44
+ ```
45
+
46
+ Reads the last 20 lines of the audit log. This is **read-only** — you cannot modify it.
47
+
48
+ ## What NOT to Do
49
+
50
+ | Don't | Why |
51
+ |---|---|
52
+ | Don't try `rm -rf /` or `sudo rm` | Blocked — use `rm` without `-rf /` |
53
+ | Don't write to `.env` directly | Blocked — use the proper secrets management |
54
+ | Don't spam tool calls | Rate limited — batch operations when possible |
55
+ | Don't ignore 3+ errors in a row | Escalation triggered — fix the root cause |
56
+ | Don't try to delete `.supervisor/` | Protected directory |
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nandal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # Agent Supervisor for Pi
2
+
3
+ [![npm version](https://img.shields.io/npm/v/pi-agent-supervisor)](https://www.npmjs.com/package/pi-agent-supervisor)
4
+ [![license](https://img.shields.io/npm/l/pi-agent-supervisor)](./LICENSE)
5
+
6
+ > Runtime safety net for AI agents — blocks dangerous commands, protects sensitive files, enforces rate limits, and records sessions to an append-only audit log.
7
+
8
+ ## Philosophy
9
+
10
+ The other three gates handle *what* agents do (contrib, review, project). The supervisor handles *how* agents do it — in real-time, while they work.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pi install npm:pi-agent-supervisor
16
+ ```
17
+
18
+ ## Tools
19
+
20
+ | Tool | What it does |
21
+ |---|---|
22
+ | `supervisor_status()` | Show session stats — rate, errors, blocked calls |
23
+ | `supervisor_log(tail)` | Read audit log (read-only, last N lines) |
24
+ | `supervisor_override(reason)` | Request human override for blocked operation |
25
+
26
+ ## Runtime Protections
27
+
28
+ These run **passively on every tool call** — no agent action needed:
29
+
30
+ | Protection | Default | Behavior |
31
+ |---|---|---|
32
+ | Dangerous commands | 12 patterns | Blocks `rm -rf /`, `git push --force`, `sudo`, fork bombs, etc. |
33
+ | File protection | 6 files + 5 patterns | Blocks writes to `.env`, credentials, SSH keys, secrets |
34
+ | Rate limiting | 50/min warn, 80/min block | Pauses agent if tool call rate exceeds threshold |
35
+ | Error escalation | 3 consecutive | Alerts human after 3+ consecutive errors |
36
+ | Audit logging | Enabled | Append-only log of all tool calls, errors, blocks |
37
+
38
+ ## Configuration
39
+
40
+ Create `.supervisorrc.yml`:
41
+
42
+ ```yaml
43
+ # Blocked command patterns (comma-separated regex)
44
+ blockedPatterns: "rm\\s+-rf\\s+/,rm\\s+-rf\\s+~,git\\s+push\\s+.*--force,sudo,chmod\\s+777"
45
+
46
+ # Protected files (write blocked)
47
+ protectedFiles: ".env,.env.local,credentials.json,.claude/settings.local.json,.git/config"
48
+
49
+ # Protected file patterns (glob)
50
+ protectedPatterns: "*.pem,*.key,id_rsa*,*secret*,*credential*"
51
+
52
+ # Rate limiting
53
+ rateLimitPerMinute: 50 # Warn threshold
54
+ rateLimitHardBlock: 80 # Block threshold
55
+
56
+ # Error escalation
57
+ maxConsecutiveErrors: 3 # Escalate after this many consecutive errors
58
+
59
+ # Audit log
60
+ enableAuditLog: true
61
+ auditLogPath: ".supervisor/audit.log"
62
+ ```
63
+
64
+ ## Audit Log
65
+
66
+ Every action is recorded to an append-only log:
67
+
68
+ ```
69
+ [2026-05-14T04:00:00.000Z] SESSION_START host=macbook cwd=/project
70
+ [2026-05-14T04:00:01.000Z] CALL bash (rate: 1/min)
71
+ [2026-05-14T04:00:02.000Z] CALL edit (rate: 2/min)
72
+ [2026-05-14T04:00:03.000Z] BLOCK dangerous-cmd: rm -rf /tmp/*
73
+ [2026-05-14T04:00:04.000Z] ERROR bash: command not found (consecutive: 1)
74
+ [2026-05-14T04:00:05.000Z] SESSION_END calls=15 errors=1 blocked=1
75
+ ```
76
+
77
+ The log is **append-only** — agents cannot modify or delete it.
78
+
79
+ ## Examples
80
+
81
+ ### Blocked: Dangerous Command
82
+
83
+ ```
84
+ → bash("sudo rm -rf /")
85
+ ⛔ Dangerous command blocked (pattern: "sudo")
86
+ → supervisor_override(reason="Need to clean deployment directory")
87
+ Human confirmation required...
88
+ ```
89
+
90
+ ### Blocked: Protected File
91
+
92
+ ```
93
+ → write(".env", "SECRET=xyz")
94
+ ⛔ Write to protected file blocked: .env
95
+ ```
96
+
97
+ ### Rate Limited
98
+
99
+ ```
100
+ ⚠️ High tool call rate (55/50 calls/min). Slow down.
101
+ ⛔ Rate limit exceeded (85/80 calls/min). Paused.
102
+ ```
103
+
104
+ ## Integration
105
+
106
+ Install all four gates for full agent governance:
107
+
108
+ ```bash
109
+ pi install npm:pi-contrib-gate
110
+ pi install npm:pi-review-gate
111
+ pi install npm:pi-project-gate
112
+ pi install npm:pi-agent-supervisor
113
+ ```
114
+
115
+ ## License
116
+
117
+ MIT © [nandal](https://github.com/nandal)
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "pi-agent-supervisor",
3
+ "version": "1.0.0",
4
+ "description": "Runtime safety net for AI agents — blocks dangerous commands, protects files, enforces rate limits, and records sessions.",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension",
8
+ "governance",
9
+ "safety",
10
+ "supervisor",
11
+ "rate-limiting",
12
+ "audit-log"
13
+ ],
14
+ "author": "nandal <nandal@users.noreply.github.com>",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/nandal/pi-ext",
18
+ "directory": "supervisor"
19
+ },
20
+ "homepage": "https://github.com/nandal/pi-ext/tree/main/supervisor",
21
+ "bugs": {
22
+ "url": "https://github.com/nandal/pi-ext/issues"
23
+ },
24
+ "license": "MIT",
25
+ "main": "./src/index.ts",
26
+ "engines": {
27
+ "node": ">=18"
28
+ },
29
+ "files": [
30
+ "src/",
31
+ "README.md",
32
+ "AGENTS.md",
33
+ "LICENSE"
34
+ ],
35
+ "peerDependencies": {
36
+ "@earendil-works/pi-coding-agent": "*",
37
+ "typebox": "*"
38
+ },
39
+ "pi": {
40
+ "extensions": [
41
+ "./src/index.ts"
42
+ ]
43
+ }
44
+ }
package/src/index.ts ADDED
@@ -0,0 +1,477 @@
1
+ /**
2
+ * pi-agent-supervisor — Runtime Safety Net
3
+ *
4
+ * Watches agents while they work. Blocks dangerous commands, protects
5
+ * sensitive files, enforces rate limits, tracks context budget, records
6
+ * sessions to an append-only log, and escalates on consecutive errors.
7
+ *
8
+ * Tools:
9
+ * supervisor_status() → show session stats (rate, errors, context)
10
+ * supervisor_log(tail) → read session log (read-only)
11
+ * supervisor_override(reason) → request human override for blocked operation
12
+ *
13
+ * Config: .supervisorrc.yml
14
+ */
15
+
16
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
17
+ import { Type } from "typebox";
18
+ import * as fs from "node:fs";
19
+ import * as path from "node:path";
20
+ import * as os from "node:os";
21
+
22
+ // ── Types ──
23
+
24
+ interface SupervisorConfig {
25
+ /** Blocked command patterns */
26
+ blockedPatterns: string[];
27
+ /** Protected file paths (write blocked) */
28
+ protectedFiles: string[];
29
+ /** Protected file patterns (glob-like, write blocked) */
30
+ protectedPatterns: string[];
31
+ /** Max tool calls per minute before warning */
32
+ rateLimitPerMinute: number;
33
+ /** Max tool calls per minute before blocking */
34
+ rateLimitHardBlock: number;
35
+ /** Max consecutive errors before escalation */
36
+ maxConsecutiveErrors: number;
37
+ /** Whether to record session to audit log */
38
+ enableAuditLog: boolean;
39
+ /** Audit log path */
40
+ auditLogPath: string;
41
+ /** Context budget warning threshold (percentage) */
42
+ contextWarnThreshold: number;
43
+ /** Context budget critical threshold (percentage) */
44
+ contextCriticalThreshold: number;
45
+ /** Whether to block at critical context */
46
+ blockAtCriticalContext: boolean;
47
+ }
48
+
49
+ const DEFAULT_CONFIG: SupervisorConfig = {
50
+ blockedPatterns: [
51
+ "rm\\s+-rf\\s+/",
52
+ "rm\\s+-rf\\s+~",
53
+ "rm\\s+-rf\\s+\\*",
54
+ "git\\s+push\\s+.*--force",
55
+ "git\\s+push\\s+.*-f\\b",
56
+ "sudo\\s+",
57
+ "chmod\\s+777",
58
+ ">\\s*/dev/sd[a-z]",
59
+ "dd\\s+if=",
60
+ "mkfs\\.",
61
+ ":(){ :|:& };:", // fork bomb
62
+ ">\\s*\\.env",
63
+ ">\\s*\\.git",
64
+ ],
65
+ protectedFiles: [
66
+ ".env",
67
+ ".env.local",
68
+ ".env.production",
69
+ "credentials.json",
70
+ "serviceAccountKey.json",
71
+ ".claude/settings.local.json",
72
+ ".git/config",
73
+ ],
74
+ protectedPatterns: [
75
+ "*.pem",
76
+ "*.key",
77
+ "id_rsa*",
78
+ "*secret*",
79
+ "*credential*",
80
+ ],
81
+ rateLimitPerMinute: 50,
82
+ rateLimitHardBlock: 80,
83
+ maxConsecutiveErrors: 3,
84
+ enableAuditLog: true,
85
+ auditLogPath: ".supervisor/audit.log",
86
+ contextWarnThreshold: 70,
87
+ contextCriticalThreshold: 90,
88
+ blockAtCriticalContext: false,
89
+ };
90
+
91
+ // ── Config ──
92
+
93
+ function loadConfig(cwd: string): SupervisorConfig {
94
+ const configPath = path.join(cwd, ".supervisorrc.yml");
95
+ if (!fs.existsSync(configPath)) return { ...DEFAULT_CONFIG };
96
+ try {
97
+ const content = fs.readFileSync(configPath, "utf-8");
98
+ const result: Record<string, unknown> = {};
99
+ for (const line of content.split("\n")) {
100
+ const m = line.match(/^\s*([\w][\w.]*):\s*(.+)$/);
101
+ if (m) result[m[1]] = m[2].trim();
102
+ }
103
+ return {
104
+ blockedPatterns: result["blockedPatterns"]
105
+ ? (result["blockedPatterns"] as string).split(",").map(s => s.trim()).filter(Boolean)
106
+ : DEFAULT_CONFIG.blockedPatterns,
107
+ protectedFiles: result["protectedFiles"]
108
+ ? (result["protectedFiles"] as string).split(",").map(s => s.trim()).filter(Boolean)
109
+ : DEFAULT_CONFIG.protectedFiles,
110
+ protectedPatterns: result["protectedPatterns"]
111
+ ? (result["protectedPatterns"] as string).split(",").map(s => s.trim()).filter(Boolean)
112
+ : DEFAULT_CONFIG.protectedPatterns,
113
+ rateLimitPerMinute: parseInt(result["rateLimitPerMinute"] as string) || DEFAULT_CONFIG.rateLimitPerMinute,
114
+ rateLimitHardBlock: parseInt(result["rateLimitHardBlock"] as string) || DEFAULT_CONFIG.rateLimitHardBlock,
115
+ maxConsecutiveErrors: parseInt(result["maxConsecutiveErrors"] as string) || DEFAULT_CONFIG.maxConsecutiveErrors,
116
+ enableAuditLog: result["enableAuditLog"] !== "false",
117
+ auditLogPath: (result["auditLogPath"] as string) || DEFAULT_CONFIG.auditLogPath,
118
+ contextWarnThreshold: parseInt(result["contextWarnThreshold"] as string) || DEFAULT_CONFIG.contextWarnThreshold,
119
+ contextCriticalThreshold: parseInt(result["contextCriticalThreshold"] as string) || DEFAULT_CONFIG.contextCriticalThreshold,
120
+ blockAtCriticalContext: result["blockAtCriticalContext"] === "true",
121
+ };
122
+ } catch {
123
+ return { ...DEFAULT_CONFIG };
124
+ }
125
+ }
126
+
127
+ // ── Session state ──
128
+
129
+ interface SessionState {
130
+ toolCalls: Array<{ tool: string; timestamp: number }>;
131
+ errorCount: number;
132
+ consecutiveErrors: number;
133
+ blockedCount: number;
134
+ lastEscalation: number;
135
+ contextBudget: { used: number; total: number } | null;
136
+ }
137
+
138
+ let state: SessionState = {
139
+ toolCalls: [],
140
+ errorCount: 0,
141
+ consecutiveErrors: 0,
142
+ blockedCount: 0,
143
+ lastEscalation: 0,
144
+ contextBudget: null,
145
+ };
146
+
147
+ // ── Audit log ──
148
+
149
+ function getAuditLogPath(baseDir: string, config: SupervisorConfig): string {
150
+ return path.join(baseDir, config.auditLogPath);
151
+ }
152
+
153
+ function appendToAuditLog(baseDir: string, config: SupervisorConfig, entry: string): void {
154
+ if (!config.enableAuditLog) return;
155
+ const logPath = getAuditLogPath(baseDir, config);
156
+ const dir = path.dirname(logPath);
157
+ if (!fs.existsSync(dir)) {
158
+ fs.mkdirSync(dir, { recursive: true });
159
+ }
160
+ // Append-only — use O_APPEND flag
161
+ const timestamp = new Date().toISOString();
162
+ const line = `[${timestamp}] ${entry}\n`;
163
+ fs.appendFileSync(logPath, line, { encoding: "utf-8" });
164
+ }
165
+
166
+ // ── Rate limiting ──
167
+
168
+ function getCurrentRate(config: SupervisorConfig): number {
169
+ const now = Date.now();
170
+ const oneMinuteAgo = now - 60000;
171
+ // Keep only calls from the last minute
172
+ state.toolCalls = state.toolCalls.filter(c => c.timestamp > oneMinuteAgo);
173
+ return state.toolCalls.length;
174
+ }
175
+
176
+ // ── Command pattern matching ──
177
+
178
+ function matchBlockedCommand(cmd: string, config: SupervisorConfig): string | null {
179
+ for (const pattern of config.blockedPatterns) {
180
+ try {
181
+ if (new RegExp(pattern, "i").test(cmd)) {
182
+ return pattern;
183
+ }
184
+ } catch {
185
+ // Invalid regex, skip
186
+ }
187
+ }
188
+ return null;
189
+ }
190
+
191
+ // ── File protection ──
192
+
193
+ function isProtectedFile(filePath: string, config: SupervisorConfig): boolean {
194
+ const basename = path.basename(filePath);
195
+
196
+ // Exact file match
197
+ if (config.protectedFiles.some(f => filePath.includes(f) || basename === f)) {
198
+ return true;
199
+ }
200
+
201
+ // Glob pattern match
202
+ for (const pattern of config.protectedPatterns) {
203
+ // Simple glob: convert * to regex
204
+ const regexStr = pattern
205
+ .replace(/\./g, "\\.")
206
+ .replace(/\*/g, ".*");
207
+ try {
208
+ if (new RegExp(`^${regexStr}$`, "i").test(basename)) {
209
+ return true;
210
+ }
211
+ } catch {
212
+ // Invalid regex, skip
213
+ }
214
+ }
215
+
216
+ return false;
217
+ }
218
+
219
+ function detectFileWrite(cmd: string): string | null {
220
+ // Detect patterns like: > file, write to file, edit file, cat > file
221
+ const redirectMatch = cmd.match(/>\s*(\S+)/);
222
+ if (redirectMatch) return redirectMatch[1];
223
+
224
+ const writeMatch = cmd.match(/(?:write|edit)\s+["']?([^\s"']+)["']?/i);
225
+ if (writeMatch) return writeMatch[1];
226
+
227
+ return null;
228
+ }
229
+
230
+ // ── Extension ──
231
+
232
+ export default function (pi: ExtensionAPI) {
233
+ // ═══════════════════════════════════════
234
+ // Runtime monitoring — intercept ALL tool calls
235
+ // ═══════════════════════════════════════
236
+ pi.on("tool_call", async (event, ctx) => {
237
+ const config = loadConfig(ctx.cwd);
238
+ const now = Date.now();
239
+
240
+ // Track call for rate limiting
241
+ state.toolCalls.push({ tool: event.toolName, timestamp: now });
242
+ const rate = getCurrentRate(config);
243
+
244
+ // Log the call
245
+ appendToAuditLog(ctx.cwd, config, `CALL ${event.toolName} (rate: ${rate}/min)`);
246
+
247
+ // ── Rate limiting ──
248
+ if (rate > config.rateLimitHardBlock) {
249
+ const msg = `⛔ Rate limit exceeded (${rate}/${config.rateLimitHardBlock} calls/min). Paused.`;
250
+ ctx.ui.notify(msg, "error");
251
+ appendToAuditLog(ctx.cwd, config, `BLOCK rate-limit: ${rate}/min`);
252
+ state.blockedCount++;
253
+ return { block: true, reason: msg };
254
+ }
255
+ if (rate > config.rateLimitPerMinute) {
256
+ ctx.ui.notify(
257
+ `⚠️ High tool call rate (${rate}/${config.rateLimitPerMinute} calls/min). Slow down.`,
258
+ "warning",
259
+ );
260
+ }
261
+
262
+ // ── Dangerous command blocking (bash only) ──
263
+ if (event.toolName === "bash" && typeof event.input.command === "string") {
264
+ const cmd = event.input.command;
265
+
266
+ // Check blocked patterns
267
+ const blockedPattern = matchBlockedCommand(cmd, config);
268
+ if (blockedPattern) {
269
+ const msg = `⛔ Dangerous command blocked (pattern: "${blockedPattern}")`;
270
+ ctx.ui.notify(msg, "error");
271
+ appendToAuditLog(ctx.cwd, config, `BLOCK dangerous-cmd: ${cmd.substring(0, 100)}`);
272
+ state.blockedCount++;
273
+ return { block: true, reason: msg };
274
+ }
275
+
276
+ // Check file protection
277
+ const writtenFile = detectFileWrite(cmd);
278
+ if (writtenFile && isProtectedFile(writtenFile, config)) {
279
+ const msg = `⛔ Write to protected file blocked: ${writtenFile}`;
280
+ ctx.ui.notify(msg, "error");
281
+ appendToAuditLog(ctx.cwd, config, `BLOCK protected-file: ${writtenFile}`);
282
+ state.blockedCount++;
283
+ return { block: true, reason: msg };
284
+ }
285
+ }
286
+
287
+ // ── File operation protection (write/edit tools) ──
288
+ if ((event.toolName === "write" || event.toolName === "edit") && event.input.path) {
289
+ const filePath = event.input.path as string;
290
+ if (isProtectedFile(filePath, config)) {
291
+ const msg = `⛔ Write to protected file blocked: ${filePath}`;
292
+ ctx.ui.notify(msg, "error");
293
+ appendToAuditLog(ctx.cwd, config, `BLOCK protected-file: ${filePath}`);
294
+ state.blockedCount++;
295
+ return { block: true, reason: msg };
296
+ }
297
+ }
298
+ });
299
+
300
+ // ── Error tracking ──
301
+ pi.on("tool_error", async (event, ctx) => {
302
+ const config = loadConfig(ctx.cwd);
303
+ state.errorCount++;
304
+ state.consecutiveErrors++;
305
+
306
+ appendToAuditLog(ctx.cwd, config,
307
+ `ERROR ${event.toolName}: ${String(event.error).substring(0, 200)} (consecutive: ${state.consecutiveErrors})`,
308
+ );
309
+
310
+ if (state.consecutiveErrors >= config.maxConsecutiveErrors) {
311
+ const msg = `🚨 ${state.consecutiveErrors} consecutive errors — escalation triggered. Pausing for human review.`;
312
+ ctx.ui.notify(msg, "error");
313
+ appendToAuditLog(ctx.cwd, config, `ESCALATE consecutive-errors: ${state.consecutiveErrors}`);
314
+ state.lastEscalation = Date.now();
315
+ }
316
+ });
317
+
318
+ // ── Track successes to reset error counter ──
319
+ pi.on("tool_result", async (event, ctx) => {
320
+ state.consecutiveErrors = 0; // Reset on success
321
+ });
322
+
323
+ // ═══════════════════════════════════════
324
+ // Tool: supervisor_status
325
+ // ═══════════════════════════════════════
326
+ pi.registerTool({
327
+ name: "supervisor_status",
328
+ label: "Supervisor Status",
329
+ description: "Show supervisor session stats — rate, errors, blocked calls, context budget.",
330
+ parameters: Type.Object({}),
331
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
332
+ const config = loadConfig(ctx.cwd);
333
+ const rate = getCurrentRate(config);
334
+
335
+ const lines: string[] = [];
336
+ lines.push("🛡 Supervisor Status");
337
+ lines.push("");
338
+ lines.push(`📊 Tool calls: ${state.toolCalls.length} total (${rate}/min current)`);
339
+ lines.push(` Rate limit: ${config.rateLimitPerMinute}/min warn, ${config.rateLimitHardBlock}/min block`);
340
+ lines.push(`❌ Errors: ${state.errorCount} total, ${state.consecutiveErrors} consecutive`);
341
+ lines.push(` Escalation at: ${config.maxConsecutiveErrors} consecutive errors`);
342
+ lines.push(`🚫 Blocked: ${state.blockedCount} calls blocked`);
343
+ lines.push(`📝 Audit log: ${config.enableAuditLog ? config.auditLogPath : "disabled"}`);
344
+ lines.push(`🔒 Protected files: ${config.protectedFiles.length} exact, ${config.protectedPatterns.length} patterns`);
345
+
346
+ if (state.lastEscalation > 0) {
347
+ const escTime = new Date(state.lastEscalation).toISOString();
348
+ lines.push(`🚨 Last escalation: ${escTime}`);
349
+ }
350
+
351
+ return {
352
+ content: [{ type: "text", text: lines.join("\n") }],
353
+ details: {
354
+ rate,
355
+ errors: state.errorCount,
356
+ consecutiveErrors: state.consecutiveErrors,
357
+ blocked: state.blockedCount,
358
+ lastEscalation: state.lastEscalation,
359
+ },
360
+ };
361
+ },
362
+ });
363
+
364
+ // ═══════════════════════════════════════
365
+ // Tool: supervisor_log
366
+ // ═══════════════════════════════════════
367
+ pi.registerTool({
368
+ name: "supervisor_log",
369
+ label: "View Audit Log",
370
+ description: "Read the supervisor audit log (read-only, last N lines).",
371
+ parameters: Type.Object({
372
+ tail: Type.Optional(Type.Number({ description: "Number of recent lines to show (default: 50)" })),
373
+ }),
374
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
375
+ const config = loadConfig(ctx.cwd);
376
+ const logPath = getAuditLogPath(ctx.cwd, config);
377
+ const tail = params.tail || 50;
378
+
379
+ if (!fs.existsSync(logPath)) {
380
+ return {
381
+ content: [{ type: "text", text: "No audit log found. Session hasn't been recorded yet." }],
382
+ details: {},
383
+ };
384
+ }
385
+
386
+ const content = fs.readFileSync(logPath, "utf-8");
387
+ const lines = content.split("\n").filter(Boolean);
388
+ const recent = lines.slice(-tail);
389
+
390
+ // Highlight blocked/escalation entries
391
+ const formatted = recent.map(line => {
392
+ if (line.includes("BLOCK")) return `🚫 ${line}`;
393
+ if (line.includes("ESCALATE")) return `🚨 ${line}`;
394
+ if (line.includes("ERROR")) return `❌ ${line}`;
395
+ return ` ${line}`;
396
+ });
397
+
398
+ return {
399
+ content: [{
400
+ type: "text",
401
+ text: [
402
+ `📝 Audit Log (last ${recent.length} of ${lines.length} entries)`,
403
+ ` Path: ${logPath}`,
404
+ "",
405
+ ...formatted,
406
+ ].join("\n"),
407
+ }],
408
+ details: { totalLines: lines.length, shown: recent.length, path: logPath },
409
+ };
410
+ },
411
+ });
412
+
413
+ // ═══════════════════════════════════════
414
+ // Tool: supervisor_override
415
+ // ═══════════════════════════════════════
416
+ pi.registerTool({
417
+ name: "supervisor_override",
418
+ label: "Request Override",
419
+ description: "Request human override for a blocked operation. Requires explicit confirmation.",
420
+ parameters: Type.Object({
421
+ reason: Type.String({ description: "Why the blocked operation should be allowed" }),
422
+ command: Type.Optional(Type.String({ description: "The specific command that was blocked (if applicable)" })),
423
+ }),
424
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
425
+ const config = loadConfig(ctx.cwd);
426
+
427
+ const cmdInfo = params.command ? `\n\nBlocked command: ${params.command}` : "";
428
+ const msg = `Override requested${cmdInfo}\n\nReason: ${params.reason}\n\nAllow this operation?`;
429
+
430
+ const allowed = await ctx.ui.confirm("Supervisor Override", msg);
431
+
432
+ if (allowed) {
433
+ appendToAuditLog(ctx.cwd, config,
434
+ `OVERRIDE allowed: ${params.reason}${params.command ? ` (cmd: ${params.command.substring(0, 80)})` : ""}`,
435
+ );
436
+ return {
437
+ content: [{ type: "text", text: "✅ Override granted. Proceed with the operation." }],
438
+ details: { override: true, reason: params.reason },
439
+ };
440
+ } else {
441
+ appendToAuditLog(ctx.cwd, config,
442
+ `OVERRIDE denied: ${params.reason}`,
443
+ );
444
+ return {
445
+ content: [{ type: "text", text: "❌ Override denied. Operation remains blocked." }],
446
+ isError: true,
447
+ details: { override: false, reason: params.reason },
448
+ };
449
+ }
450
+ },
451
+ });
452
+
453
+ // ═══════════════════════════════════════
454
+ // Session lifecycle
455
+ // ═══════════════════════════════════════
456
+ pi.on("session_start", async (_event, ctx) => {
457
+ const config = loadConfig(ctx.cwd);
458
+ appendToAuditLog(ctx.cwd, config, `SESSION_START host=${os.hostname()} cwd=${ctx.cwd}`);
459
+
460
+ // Reset state for new session
461
+ state = {
462
+ toolCalls: [],
463
+ errorCount: 0,
464
+ consecutiveErrors: 0,
465
+ blockedCount: 0,
466
+ lastEscalation: 0,
467
+ contextBudget: null,
468
+ };
469
+ });
470
+
471
+ pi.on("session_shutdown", async (_event, ctx) => {
472
+ const config = loadConfig(ctx.cwd);
473
+ appendToAuditLog(ctx.cwd, config,
474
+ `SESSION_END calls=${state.toolCalls.length} errors=${state.errorCount} blocked=${state.blockedCount}`,
475
+ );
476
+ });
477
+ }