@os-eco/overstory-cli 0.7.0 → 0.7.2

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.
Files changed (60) hide show
  1. package/README.md +6 -5
  2. package/agents/builder.md +1 -1
  3. package/agents/coordinator.md +12 -11
  4. package/agents/lead.md +6 -6
  5. package/agents/monitor.md +4 -4
  6. package/agents/reviewer.md +1 -1
  7. package/agents/scout.md +5 -5
  8. package/agents/supervisor.md +36 -32
  9. package/package.json +1 -1
  10. package/src/agents/guard-rules.ts +97 -0
  11. package/src/agents/hooks-deployer.ts +7 -90
  12. package/src/agents/overlay.test.ts +7 -7
  13. package/src/agents/overlay.ts +5 -5
  14. package/src/commands/agents.test.ts +5 -0
  15. package/src/commands/clean.test.ts +3 -0
  16. package/src/commands/completions.ts +1 -1
  17. package/src/commands/coordinator.test.ts +1 -0
  18. package/src/commands/coordinator.ts +15 -11
  19. package/src/commands/costs.test.ts +5 -0
  20. package/src/commands/init.test.ts +1 -2
  21. package/src/commands/init.ts +1 -8
  22. package/src/commands/inspect.test.ts +14 -0
  23. package/src/commands/log.test.ts +14 -0
  24. package/src/commands/log.ts +39 -0
  25. package/src/commands/mail.test.ts +5 -0
  26. package/src/commands/monitor.ts +15 -11
  27. package/src/commands/nudge.test.ts +1 -0
  28. package/src/commands/prime.test.ts +2 -0
  29. package/src/commands/prime.ts +6 -2
  30. package/src/commands/run.test.ts +1 -0
  31. package/src/commands/sling.test.ts +15 -1
  32. package/src/commands/sling.ts +44 -21
  33. package/src/commands/status.test.ts +1 -0
  34. package/src/commands/stop.test.ts +1 -0
  35. package/src/commands/supervisor.ts +19 -12
  36. package/src/commands/trace.test.ts +1 -0
  37. package/src/commands/worktree.test.ts +9 -0
  38. package/src/config.ts +29 -0
  39. package/src/doctor/consistency.test.ts +14 -0
  40. package/src/e2e/init-sling-lifecycle.test.ts +3 -5
  41. package/src/index.ts +1 -1
  42. package/src/mail/broadcast.test.ts +1 -0
  43. package/src/merge/resolver.ts +23 -4
  44. package/src/runtimes/claude.test.ts +1 -1
  45. package/src/runtimes/pi-guards.test.ts +433 -0
  46. package/src/runtimes/pi-guards.ts +349 -0
  47. package/src/runtimes/pi.test.ts +620 -0
  48. package/src/runtimes/pi.ts +244 -0
  49. package/src/runtimes/registry.test.ts +33 -0
  50. package/src/runtimes/registry.ts +15 -2
  51. package/src/runtimes/types.ts +63 -0
  52. package/src/schema-consistency.test.ts +1 -0
  53. package/src/sessions/compat.ts +1 -0
  54. package/src/sessions/store.test.ts +31 -0
  55. package/src/sessions/store.ts +37 -4
  56. package/src/types.ts +17 -0
  57. package/src/watchdog/daemon.test.ts +7 -4
  58. package/src/watchdog/daemon.ts +1 -1
  59. package/src/watchdog/health.test.ts +1 -0
  60. package/src/watchdog/triage.ts +14 -4
@@ -0,0 +1,349 @@
1
+ // Pi runtime guard extension generator.
2
+ // Generates self-contained TypeScript code for .pi/extensions/overstory-guard.ts.
3
+ //
4
+ // Pi's extension system uses the ExtensionAPI factory style:
5
+ // export default function(pi: ExtensionAPI) { pi.on("event", handler) }
6
+ //
7
+ // Guards fire via pi.on("tool_call", ...) and return { block: true, reason }
8
+ // to prevent tool execution — equivalent to Claude Code's PreToolUse hooks.
9
+ //
10
+ // Activity tracking fires via pi.exec("ov log ...") on tool_call,
11
+ // tool_execution_end, and session_shutdown events so the SessionStore
12
+ // lastActivity stays fresh and the watchdog does not zombie-classify agents.
13
+
14
+ import {
15
+ DANGEROUS_BASH_PATTERNS,
16
+ INTERACTIVE_TOOLS,
17
+ NATIVE_TEAM_TOOLS,
18
+ SAFE_BASH_PREFIXES,
19
+ WRITE_TOOLS,
20
+ } from "../agents/guard-rules.ts";
21
+ import { extractQualityGatePrefixes } from "../agents/hooks-deployer.ts";
22
+ import { DEFAULT_QUALITY_GATES } from "../config.ts";
23
+ import type { HooksDef } from "./types.ts";
24
+
25
+ /** Capabilities that must not modify project files. */
26
+ const NON_IMPLEMENTATION_CAPABILITIES = new Set([
27
+ "scout",
28
+ "reviewer",
29
+ "lead",
30
+ "coordinator",
31
+ "supervisor",
32
+ "monitor",
33
+ ]);
34
+
35
+ /** Coordination capabilities that get git add/commit whitelisted for metadata sync. */
36
+ const COORDINATION_CAPABILITIES = new Set(["coordinator", "supervisor", "monitor"]);
37
+
38
+ /**
39
+ * Bash patterns that modify files and require path boundary validation.
40
+ * Mirrors FILE_MODIFYING_BASH_PATTERNS in hooks-deployer.ts (not exported, duplicated here).
41
+ * Applied to implementation agents (builder/merger) only.
42
+ */
43
+ const FILE_MODIFYING_BASH_PATTERNS = [
44
+ "sed\\s+-i",
45
+ "sed\\s+--in-place",
46
+ "echo\\s+.*>",
47
+ "printf\\s+.*>",
48
+ "cat\\s+.*>",
49
+ "tee\\s",
50
+ "\\bmv\\s",
51
+ "\\bcp\\s",
52
+ "\\brm\\s",
53
+ "\\bmkdir\\s",
54
+ "\\btouch\\s",
55
+ "\\bchmod\\s",
56
+ "\\bchown\\s",
57
+ ">>",
58
+ "\\binstall\\s",
59
+ "\\brsync\\s",
60
+ ];
61
+
62
+ /** Serialize a string array as a TypeScript Set<string> literal (tab-indented entries). */
63
+ function toSetLiteral(items: string[]): string {
64
+ if (items.length === 0) return "new Set<string>([])";
65
+ const entries = items.map((s) => `\t"${s}",`).join("\n");
66
+ return `new Set<string>([\n${entries}\n])`;
67
+ }
68
+
69
+ /** Serialize a string array as a TypeScript string[] literal (tab-indented entries). */
70
+ function toStringArrayLiteral(items: string[]): string {
71
+ if (items.length === 0) return "[]";
72
+ const entries = items.map((s) => `\t"${s}",`).join("\n");
73
+ return `[\n${entries}\n]`;
74
+ }
75
+
76
+ /**
77
+ * Serialize grep -qE pattern strings as a TypeScript RegExp[] literal.
78
+ * Pattern strings use \\b/\\s double-escaping: their string values (\b/\s) map
79
+ * directly to JavaScript regex word boundary/whitespace tokens.
80
+ */
81
+ function toRegExpArrayLiteral(patterns: string[]): string {
82
+ if (patterns.length === 0) return "[]";
83
+ const entries = patterns.map((p) => `\t/${p}/,`).join("\n");
84
+ return `[\n${entries}\n]`;
85
+ }
86
+
87
+ /**
88
+ * Generate a self-contained TypeScript guard extension for Pi's extension system.
89
+ *
90
+ * The returned string is ready to write as `.pi/extensions/overstory-guard.ts`.
91
+ * Pi loads this file and calls the default export with an ExtensionAPI instance.
92
+ *
93
+ * Extension uses the correct Pi factory style:
94
+ * export default function(pi: ExtensionAPI) { pi.on("event", handler); }
95
+ *
96
+ * Guard order (per AgentRuntime spec):
97
+ * 1. Block NATIVE_TEAM_TOOLS (all agents) — use ov sling for delegation.
98
+ * (Safety net: Pi does not use Claude Code's native team tools, so these
99
+ * are no-ops unless a future Pi version adds similar tool names.)
100
+ * 2. Block INTERACTIVE_TOOLS (all agents) — escalate via ov mail instead.
101
+ * (Safety net: Pi does not have AskUserQuestion/EnterPlanMode natively.)
102
+ * 3. Block write tools for non-implementation capabilities.
103
+ * (Pi uses lowercase tool names: "write", "edit" — checked in addition to
104
+ * the original mixed-case Claude Code names for forward compatibility.)
105
+ * 4. Path boundary on write/edit tools (all agents, defense-in-depth).
106
+ * (Pi uses event.input.path, not file_path.)
107
+ * 5. Universal Bash danger guards: git push, reset --hard, wrong branch naming.
108
+ * (Pi bash tool is named "bash" in lowercase.)
109
+ * 6a. Non-implementation agents: safe prefix whitelist then dangerous pattern blocklist.
110
+ * 6b. Implementation agents (builder/merger): file-modifying bash path boundary.
111
+ * 7. Default allow.
112
+ *
113
+ * Activity tracking:
114
+ * - tool_call handler: fire-and-forget "ov log tool-start" to update lastActivity.
115
+ * - tool_execution_end handler: fire-and-forget "ov log tool-end".
116
+ * - session_shutdown handler: awaited "ov log session-end" to mark agent completed.
117
+ *
118
+ * These tracking calls prevent the watchdog from zombie-classifying Pi agents due
119
+ * to stale lastActivity timestamps (the root cause of the zombie state bug).
120
+ *
121
+ * @param hooks - Agent identity, capability, worktree path, and optional quality gates.
122
+ * @returns Self-contained TypeScript source code for the Pi guard extension file.
123
+ */
124
+ export function generatePiGuardExtension(hooks: HooksDef): string {
125
+ const { agentName, capability, worktreePath, qualityGates } = hooks;
126
+ const gates = qualityGates ?? DEFAULT_QUALITY_GATES;
127
+ const gatePrefixes = extractQualityGatePrefixes(gates);
128
+
129
+ const isNonImpl = NON_IMPLEMENTATION_CAPABILITIES.has(capability);
130
+ const isCoordination = COORDINATION_CAPABILITIES.has(capability);
131
+
132
+ // Build safe Bash prefixes: base set + coordination extras + quality gate commands.
133
+ const safePrefixes: string[] = [
134
+ ...SAFE_BASH_PREFIXES,
135
+ ...(isCoordination ? ["git add", "git commit"] : []),
136
+ ...gatePrefixes,
137
+ ];
138
+
139
+ // Pi uses lowercase tool names; also include the original mixed-case names
140
+ // from WRITE_TOOLS as a safety net for any future Pi version that adopts them.
141
+ const piWriteToolsBlocked = ["write", "edit", ...WRITE_TOOLS];
142
+
143
+ const teamBlockedCode = toSetLiteral([...NATIVE_TEAM_TOOLS]);
144
+ const interactiveBlockedCode = toSetLiteral([...INTERACTIVE_TOOLS]);
145
+ const writeBlockedCode = isNonImpl ? toSetLiteral(piWriteToolsBlocked) : null;
146
+ const safePrefixesCode = toStringArrayLiteral(safePrefixes);
147
+ const dangerousPatternsCode = toRegExpArrayLiteral(DANGEROUS_BASH_PATTERNS);
148
+ const fileModifyingPatternsCode = toRegExpArrayLiteral(FILE_MODIFYING_BASH_PATTERNS);
149
+
150
+ // Capability-specific Bash guard block (mutually exclusive).
151
+ // Indented for insertion inside the "bash" tool_call branch.
152
+ const capabilityBashBlock = isNonImpl
153
+ ? [
154
+ "",
155
+ `\t\t\t// Non-implementation agents: whitelist safe prefixes, block dangerous patterns.`,
156
+ `\t\t\tconst trimmed = cmd.trimStart();`,
157
+ `\t\t\tif (SAFE_PREFIXES.some((p) => trimmed.startsWith(p))) {`,
158
+ `\t\t\t\treturn; // Safe command — allow through.`,
159
+ `\t\t\t}`,
160
+ `\t\t\tif (DANGEROUS_PATTERNS.some((re) => re.test(cmd))) {`,
161
+ `\t\t\t\treturn {`,
162
+ `\t\t\t\t\tblock: true,`,
163
+ `\t\t\t\t\treason: "${capability} agents cannot modify files — this command is not allowed",`,
164
+ `\t\t\t\t};`,
165
+ `\t\t\t}`,
166
+ ].join("\n")
167
+ : [
168
+ "",
169
+ `\t\t\t// Implementation agents: path boundary on file-modifying Bash commands.`,
170
+ `\t\t\tif (FILE_MODIFYING_PATTERNS.some((re) => re.test(cmd))) {`,
171
+ `\t\t\t\tconst tokens = cmd.split(/\\s+/);`,
172
+ `\t\t\t\tconst paths = tokens`,
173
+ `\t\t\t\t\t.filter((t) => t.startsWith("/"))`,
174
+ `\t\t\t\t\t.map((t) => t.replace(/[";>]*$/, ""));`,
175
+ `\t\t\t\tfor (const p of paths) {`,
176
+ `\t\t\t\t\tif (!p.startsWith("/dev/") && !p.startsWith("/tmp/") && !p.startsWith(WORKTREE_PATH + "/") && p !== WORKTREE_PATH) {`,
177
+ `\t\t\t\t\t\treturn {`,
178
+ `\t\t\t\t\t\t\tblock: true,`,
179
+ `\t\t\t\t\t\t\treason: "Bash path boundary violation: command targets a path outside your worktree. All file modifications must stay within your assigned worktree.",`,
180
+ `\t\t\t\t\t\t};`,
181
+ `\t\t\t\t\t}`,
182
+ `\t\t\t\t}`,
183
+ `\t\t\t}`,
184
+ ].join("\n");
185
+
186
+ const lines = [
187
+ `// .pi/extensions/overstory-guard.ts`,
188
+ `// Generated by overstory — do not edit manually.`,
189
+ `// Agent: ${agentName} | Capability: ${capability}`,
190
+ `//`,
191
+ `// Uses Pi's ExtensionAPI factory style: export default function(pi: ExtensionAPI) { ... }`,
192
+ `// pi.on("tool_call", ...) returns { block: true, reason } to prevent tool execution.`,
193
+ `// pi.exec("ov", [...]) calls the overstory CLI for activity tracking.`,
194
+ `import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";`,
195
+ ``,
196
+ `const AGENT_NAME = "${agentName}";`,
197
+ `const WORKTREE_PATH = "${worktreePath}";`,
198
+ ``,
199
+ `// Native team/task tools blocked (all agents) — use ov sling for delegation.`,
200
+ `// Safety net: Pi does not use Claude Code team tool names natively.`,
201
+ `const TEAM_BLOCKED = ${teamBlockedCode};`,
202
+ ``,
203
+ `// Interactive tools blocked (all agents) — escalate via ov mail instead.`,
204
+ `// Safety net: Pi does not use these Claude Code tool names natively.`,
205
+ `const INTERACTIVE_BLOCKED = ${interactiveBlockedCode};`,
206
+ ``,
207
+ ...(isNonImpl && writeBlockedCode !== null
208
+ ? [
209
+ `// Write tools blocked for non-implementation capabilities.`,
210
+ `// Includes Pi lowercase names ("write", "edit") and Claude Code names for compat.`,
211
+ `const WRITE_BLOCKED = ${writeBlockedCode};`,
212
+ ``,
213
+ ]
214
+ : []),
215
+ `// Write-scope tools where path boundary is enforced (all agents, defense-in-depth).`,
216
+ `// Pi uses lowercase tool names; also include Claude Code names for forward compat.`,
217
+ `const WRITE_SCOPE_TOOLS = new Set<string>(["write", "edit", "Write", "Edit", "NotebookEdit"]);`,
218
+ ``,
219
+ `// Safe Bash command prefixes — checked before the dangerous pattern blocklist.`,
220
+ `const SAFE_PREFIXES = ${safePrefixesCode};`,
221
+ ``,
222
+ `// Dangerous Bash patterns blocked for non-implementation agents.`,
223
+ `const DANGEROUS_PATTERNS = ${dangerousPatternsCode};`,
224
+ ``,
225
+ `// File-modifying Bash patterns requiring path boundary validation (implementation agents).`,
226
+ `const FILE_MODIFYING_PATTERNS = ${fileModifyingPatternsCode};`,
227
+ ``,
228
+ `export default function (pi: ExtensionAPI) {`,
229
+ `\t/**`,
230
+ `\t * Tool call guard + activity tracking.`,
231
+ `\t *`,
232
+ `\t * Fires before each tool executes. Returns { block: true, reason } to block.`,
233
+ `\t * Fire-and-forgets "ov log tool-start" to update lastActivity in the SessionStore,`,
234
+ `\t * preventing the Tier 0 watchdog from zombie-classifying this agent due to stale`,
235
+ `\t * lastActivity timestamps (the root cause of the Pi zombie state bug).`,
236
+ `\t *`,
237
+ `\t * NOTE: Pi tool names are lowercase ("bash", "write", "edit").`,
238
+ `\t * event.toolName is used (not event.name — that field does not exist on ToolCallEvent).`,
239
+ `\t * Path boundary uses event.input.path (not file_path — that is Claude Code's field name).`,
240
+ `\t */`,
241
+ `\tpi.on("tool_call", async (event) => {`,
242
+ `\t\t// Activity tracking: update lastActivity so watchdog knows agent is alive.`,
243
+ `\t\t// Fire-and-forget — do not await (avoids latency on every tool call).`,
244
+ `\t\tpi.exec("ov", ["log", "tool-start", "--agent", AGENT_NAME]).catch(() => {});`,
245
+ ``,
246
+ `\t\t// 1. Block native team/task tools (all agents).`,
247
+ `\t\tif (TEAM_BLOCKED.has(event.toolName)) {`,
248
+ `\t\t\treturn {`,
249
+ `\t\t\t\tblock: true,`,
250
+ `\t\t\t\treason: \`Overstory agents must use 'ov sling' for delegation — \${event.toolName} is not allowed\`,`,
251
+ `\t\t\t};`,
252
+ `\t\t}`,
253
+ ``,
254
+ `\t\t// 2. Block interactive tools (all agents).`,
255
+ `\t\tif (INTERACTIVE_BLOCKED.has(event.toolName)) {`,
256
+ `\t\t\treturn {`,
257
+ `\t\t\t\tblock: true,`,
258
+ `\t\t\t\treason: \`\${event.toolName} requires human interaction — use ov mail (--type question) to escalate\`,`,
259
+ `\t\t\t};`,
260
+ `\t\t}`,
261
+ ``,
262
+ ...(isNonImpl
263
+ ? [
264
+ `\t\t// 3. Block write tools for non-implementation capabilities.`,
265
+ `\t\tif (WRITE_BLOCKED.has(event.toolName)) {`,
266
+ `\t\t\treturn {`,
267
+ `\t\t\t\tblock: true,`,
268
+ `\t\t\t\treason: \`${capability} agents cannot modify files — \${event.toolName} is not allowed\`,`,
269
+ `\t\t\t};`,
270
+ `\t\t}`,
271
+ ``,
272
+ ]
273
+ : []),
274
+ `\t\t// ${isNonImpl ? "4" : "3"}. Path boundary enforcement for write/edit tools (all agents).`,
275
+ `\t\t// Pi uses event.input.path (not file_path — that is Claude Code's field name).`,
276
+ `\t\tif (WRITE_SCOPE_TOOLS.has(event.toolName)) {`,
277
+ `\t\t\tconst filePath = String(`,
278
+ `\t\t\t\t(event.input as Record<string, unknown>)?.path ??`,
279
+ `\t\t\t\t(event.input as Record<string, unknown>)?.file_path ??`,
280
+ `\t\t\t\t(event.input as Record<string, unknown>)?.notebook_path ??`,
281
+ `\t\t\t\t"",`,
282
+ `\t\t\t);`,
283
+ `\t\t\tif (filePath && !filePath.startsWith(WORKTREE_PATH + "/") && filePath !== WORKTREE_PATH) {`,
284
+ `\t\t\t\treturn {`,
285
+ `\t\t\t\t\tblock: true,`,
286
+ `\t\t\t\t\treason: "Path boundary violation: file is outside your assigned worktree. All writes must target files within your worktree.",`,
287
+ `\t\t\t\t};`,
288
+ `\t\t\t}`,
289
+ `\t\t}`,
290
+ ``,
291
+ `\t\t// ${isNonImpl ? "5" : "4"}. Bash command guards.`,
292
+ `\t\t// Pi's bash tool is named "bash" (lowercase).`,
293
+ `\t\tif (event.toolName === "bash" || event.toolName === "Bash") {`,
294
+ `\t\t\tconst cmd = String((event.input as Record<string, unknown>)?.command ?? "");`,
295
+ ``,
296
+ `\t\t\t// Universal danger guards (all agents).`,
297
+ `\t\t\tif (/\\bgit\\s+push\\b/.test(cmd)) {`,
298
+ `\t\t\t\treturn {`,
299
+ `\t\t\t\t\tblock: true,`,
300
+ `\t\t\t\t\treason: "git push is blocked — use ov merge to integrate changes, push manually when ready",`,
301
+ `\t\t\t\t};`,
302
+ `\t\t\t}`,
303
+ `\t\t\tif (/git\\s+reset\\s+--hard/.test(cmd)) {`,
304
+ `\t\t\t\treturn {`,
305
+ `\t\t\t\t\tblock: true,`,
306
+ `\t\t\t\t\treason: "git reset --hard is not allowed — it destroys uncommitted work",`,
307
+ `\t\t\t\t};`,
308
+ `\t\t\t}`,
309
+ `\t\t\tconst branchMatch = /git\\s+checkout\\s+-b\\s+(\\S+)/.exec(cmd);`,
310
+ `\t\t\tif (branchMatch) {`,
311
+ `\t\t\t\tconst branch = branchMatch[1] ?? "";`,
312
+ `\t\t\t\tif (!branch.startsWith(\`overstory/\${AGENT_NAME}/\`)) {`,
313
+ `\t\t\t\t\treturn {`,
314
+ `\t\t\t\t\t\tblock: true,`,
315
+ `\t\t\t\t\t\treason: \`Branch must follow overstory/\${AGENT_NAME}/{task-id} convention\`,`,
316
+ `\t\t\t\t\t};`,
317
+ `\t\t\t\t}`,
318
+ `\t\t\t}`,
319
+ capabilityBashBlock,
320
+ `\t\t}`,
321
+ ``,
322
+ `\t\t// Default: allow.`,
323
+ `\t});`,
324
+ ``,
325
+ `\t/**`,
326
+ `\t * Tool execution end: fire-and-forget "ov log tool-end" for event tracking.`,
327
+ `\t * Paired with tool_call's tool-start fire for proper begin/end event logging.`,
328
+ `\t */`,
329
+ `\tpi.on("tool_execution_end", async (_event) => {`,
330
+ `\t\tpi.exec("ov", ["log", "tool-end", "--agent", AGENT_NAME]).catch(() => {});`,
331
+ `\t});`,
332
+ ``,
333
+ `\t/**`,
334
+ `\t * Session shutdown: log session-end so the agent transitions to "completed" state.`,
335
+ `\t *`,
336
+ `\t * Awaited so it completes before Pi exits. Without this call, the agent stays in`,
337
+ `\t * "booting" or "working" state forever, requiring manual cleanup or watchdog termination.`,
338
+ `\t *`,
339
+ `\t * Fires on Ctrl+C, Ctrl+D, or SIGTERM.`,
340
+ `\t */`,
341
+ `\tpi.on("session_shutdown", async (_event) => {`,
342
+ `\t\tawait pi.exec("ov", ["log", "session-end", "--agent", AGENT_NAME]).catch(() => {});`,
343
+ `\t});`,
344
+ `}`,
345
+ ``,
346
+ ];
347
+
348
+ return lines.join("\n");
349
+ }