@os-eco/overstory-cli 0.6.1 → 0.6.5

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 (110) hide show
  1. package/README.md +8 -7
  2. package/package.json +12 -4
  3. package/src/agents/checkpoint.test.ts +2 -2
  4. package/src/agents/hooks-deployer.test.ts +131 -16
  5. package/src/agents/hooks-deployer.ts +33 -1
  6. package/src/agents/identity.test.ts +27 -27
  7. package/src/agents/identity.ts +10 -10
  8. package/src/agents/lifecycle.test.ts +6 -6
  9. package/src/agents/lifecycle.ts +2 -2
  10. package/src/agents/manifest.test.ts +86 -0
  11. package/src/agents/overlay.test.ts +9 -9
  12. package/src/agents/overlay.ts +4 -4
  13. package/src/commands/agents.test.ts +8 -8
  14. package/src/commands/agents.ts +62 -91
  15. package/src/commands/clean.test.ts +36 -51
  16. package/src/commands/clean.ts +28 -49
  17. package/src/commands/completions.ts +14 -0
  18. package/src/commands/coordinator.test.ts +133 -26
  19. package/src/commands/coordinator.ts +101 -64
  20. package/src/commands/costs.test.ts +47 -47
  21. package/src/commands/costs.ts +96 -75
  22. package/src/commands/dashboard.test.ts +2 -2
  23. package/src/commands/dashboard.ts +75 -95
  24. package/src/commands/doctor.test.ts +2 -2
  25. package/src/commands/doctor.ts +92 -79
  26. package/src/commands/errors.test.ts +2 -2
  27. package/src/commands/errors.ts +56 -50
  28. package/src/commands/feed.test.ts +2 -2
  29. package/src/commands/feed.ts +86 -83
  30. package/src/commands/group.ts +167 -177
  31. package/src/commands/hooks.test.ts +2 -2
  32. package/src/commands/hooks.ts +52 -42
  33. package/src/commands/init.test.ts +19 -19
  34. package/src/commands/init.ts +7 -16
  35. package/src/commands/inspect.test.ts +18 -18
  36. package/src/commands/inspect.ts +55 -58
  37. package/src/commands/log.test.ts +26 -31
  38. package/src/commands/log.ts +97 -91
  39. package/src/commands/logs.test.ts +1 -1
  40. package/src/commands/logs.ts +101 -104
  41. package/src/commands/mail.test.ts +5 -5
  42. package/src/commands/mail.ts +157 -169
  43. package/src/commands/merge.test.ts +28 -66
  44. package/src/commands/merge.ts +21 -51
  45. package/src/commands/metrics.test.ts +8 -8
  46. package/src/commands/metrics.ts +34 -35
  47. package/src/commands/monitor.test.ts +3 -3
  48. package/src/commands/monitor.ts +57 -62
  49. package/src/commands/nudge.test.ts +1 -1
  50. package/src/commands/nudge.ts +41 -89
  51. package/src/commands/prime.test.ts +19 -51
  52. package/src/commands/prime.ts +13 -50
  53. package/src/commands/replay.test.ts +2 -2
  54. package/src/commands/replay.ts +79 -86
  55. package/src/commands/run.test.ts +1 -1
  56. package/src/commands/run.ts +97 -77
  57. package/src/commands/sling.test.ts +201 -5
  58. package/src/commands/sling.ts +37 -64
  59. package/src/commands/spec.test.ts +14 -40
  60. package/src/commands/spec.ts +32 -101
  61. package/src/commands/status.test.ts +97 -1
  62. package/src/commands/status.ts +63 -58
  63. package/src/commands/stop.test.ts +22 -40
  64. package/src/commands/stop.ts +18 -33
  65. package/src/commands/supervisor.test.ts +12 -14
  66. package/src/commands/supervisor.ts +144 -165
  67. package/src/commands/trace.test.ts +15 -15
  68. package/src/commands/trace.ts +59 -82
  69. package/src/commands/watch.test.ts +2 -2
  70. package/src/commands/watch.ts +38 -45
  71. package/src/commands/worktree.test.ts +213 -37
  72. package/src/commands/worktree.ts +110 -55
  73. package/src/config.test.ts +96 -0
  74. package/src/doctor/consistency.test.ts +14 -14
  75. package/src/doctor/databases.test.ts +22 -2
  76. package/src/doctor/databases.ts +16 -0
  77. package/src/doctor/dependencies.test.ts +55 -1
  78. package/src/doctor/dependencies.ts +113 -18
  79. package/src/doctor/merge-queue.test.ts +4 -4
  80. package/src/e2e/init-sling-lifecycle.test.ts +8 -8
  81. package/src/errors.ts +1 -1
  82. package/src/index.ts +223 -213
  83. package/src/logging/color.test.ts +74 -91
  84. package/src/logging/color.ts +52 -46
  85. package/src/logging/reporter.test.ts +10 -10
  86. package/src/logging/reporter.ts +6 -5
  87. package/src/mail/broadcast.test.ts +1 -1
  88. package/src/mail/client.test.ts +6 -6
  89. package/src/mail/store.test.ts +3 -3
  90. package/src/merge/queue.test.ts +73 -7
  91. package/src/merge/queue.ts +17 -2
  92. package/src/merge/resolver.test.ts +159 -7
  93. package/src/merge/resolver.ts +46 -2
  94. package/src/metrics/store.test.ts +44 -44
  95. package/src/metrics/store.ts +2 -2
  96. package/src/metrics/summary.test.ts +35 -35
  97. package/src/mulch/client.test.ts +1 -1
  98. package/src/schema-consistency.test.ts +239 -0
  99. package/src/sessions/compat.test.ts +3 -3
  100. package/src/sessions/compat.ts +2 -2
  101. package/src/sessions/store.test.ts +41 -4
  102. package/src/sessions/store.ts +13 -2
  103. package/src/types.ts +14 -14
  104. package/src/watchdog/daemon.test.ts +10 -10
  105. package/src/watchdog/daemon.ts +1 -1
  106. package/src/watchdog/health.test.ts +1 -1
  107. package/src/worktree/manager.test.ts +20 -20
  108. package/src/worktree/manager.ts +120 -4
  109. package/src/worktree/tmux.test.ts +98 -9
  110. package/src/worktree/tmux.ts +18 -0
package/README.md CHANGED
@@ -127,6 +127,7 @@ overstory sling <task-id> Spawn a worker agent
127
127
  --parent <agent-name> Parent (for hierarchy tracking)
128
128
  --depth <n> Current hierarchy depth
129
129
  --skip-scout Skip scout phase (passed to lead overlay)
130
+ --skip-task-check Skip task existence validation
130
131
  --json JSON output
131
132
 
132
133
  overstory stop <agent-name> Terminate a running agent
@@ -221,7 +222,7 @@ overstory inspect <agent> Deep per-agent inspection
221
222
  --no-tmux Skip tmux capture
222
223
  --limit <n> Limit events shown
223
224
 
224
- overstory spec write <bead-id> Write a task specification
225
+ overstory spec write <task-id> Write a task specification
225
226
  --body <content> Spec content (or pipe via stdin)
226
227
 
227
228
  overstory errors Aggregated error view across agents
@@ -269,16 +270,16 @@ Global Flags:
269
270
  ## Tech Stack
270
271
 
271
272
  - **Runtime**: Bun (TypeScript directly, no build step)
272
- - **Dependencies**: Zero runtime dependencies only Bun built-in APIs
273
+ - **Dependencies**: Minimal runtime — `chalk` (color output), `commander` (CLI framework), core I/O via Bun built-in APIs
273
274
  - **Database**: SQLite via `bun:sqlite` (WAL mode for concurrent access)
274
275
  - **Linting**: Biome (formatter + linter)
275
- - **Testing**: `bun test` (2087 tests across 75 files, colocated with source)
276
+ - **Testing**: `bun test` (2145 tests across 76 files, colocated with source)
276
277
  - **External CLIs**: `bd` (beads) or `sd` (seeds), `mulch`, `git`, `tmux` — invoked as subprocesses
277
278
 
278
279
  ## Development
279
280
 
280
281
  ```bash
281
- # Run tests (2087 tests across 75 files)
282
+ # Run tests (2145 tests across 76 files)
282
283
  bun test
283
284
 
284
285
  # Run a single test
@@ -307,14 +308,14 @@ Use the bump script to update both:
307
308
  bun run version:bump <major|minor|patch>
308
309
  ```
309
310
 
310
- Git tags are created automatically by GitHub Actions when a version bump is pushed to `main`.
311
+ Git tags, npm publishing, and GitHub releases are handled automatically by the `publish.yml` workflow when a version bump is pushed to `main`.
311
312
 
312
313
  ## Project Structure
313
314
 
314
315
  ```
315
316
  overstory/
316
317
  src/
317
- index.ts CLI entry point (command router)
318
+ index.ts CLI entry point (Commander.js program)
318
319
  types.ts Shared types and interfaces
319
320
  config.ts Config loader + validation
320
321
  errors.ts Custom error types
@@ -322,7 +323,7 @@ overstory/
322
323
  agents.ts Agent discovery and querying
323
324
  coordinator.ts Persistent orchestrator lifecycle
324
325
  supervisor.ts Team lead management
325
- dashboard.ts Live TUI dashboard (ANSI, zero deps)
326
+ dashboard.ts Live TUI dashboard (ANSI via Chalk)
326
327
  hooks.ts Orchestrator hooks management
327
328
  sling.ts Agent spawning
328
329
  group.ts Task group batch tracking
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@os-eco/overstory-cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.5",
4
4
  "description": "Multi-agent orchestration for Claude Code — spawn worker agents in git worktrees via tmux, coordinate through SQLite mail, merge with tiered conflict resolution",
5
5
  "author": "Jaymin West",
6
6
  "license": "MIT",
@@ -21,10 +21,15 @@
21
21
  "developer-tools"
22
22
  ],
23
23
  "bin": {
24
- "overstory": "./src/index.ts"
24
+ "overstory": "./src/index.ts",
25
+ "ov": "./src/index.ts"
25
26
  },
26
27
  "main": "src/index.ts",
27
- "files": ["src", "agents", "templates"],
28
+ "files": [
29
+ "src",
30
+ "agents",
31
+ "templates"
32
+ ],
28
33
  "publishConfig": {
29
34
  "access": "public"
30
35
  },
@@ -38,7 +43,10 @@
38
43
  "typecheck": "tsc --noEmit",
39
44
  "version:bump": "bun scripts/version-bump.ts"
40
45
  },
41
- "dependencies": {},
46
+ "dependencies": {
47
+ "chalk": "^5.6.2",
48
+ "commander": "^14.0.3"
49
+ },
42
50
  "devDependencies": {
43
51
  "@types/bun": "latest",
44
52
  "typescript": "^5.9.0",
@@ -9,7 +9,7 @@ import { clearCheckpoint, loadCheckpoint, saveCheckpoint } from "./checkpoint.ts
9
9
  function makeCheckpoint(overrides?: Partial<SessionCheckpoint>): SessionCheckpoint {
10
10
  return {
11
11
  agentName: "test-agent",
12
- beadId: "overstory-abc1",
12
+ taskId: "overstory-abc1",
13
13
  sessionId: "session-001",
14
14
  timestamp: "2025-01-01T00:00:00.000Z",
15
15
  progressSummary: "Implemented checkpoint module",
@@ -40,7 +40,7 @@ describe("checkpoint", () => {
40
40
 
41
41
  expect(loaded).not.toBeNull();
42
42
  expect(loaded?.agentName).toBe("test-agent");
43
- expect(loaded?.beadId).toBe("overstory-abc1");
43
+ expect(loaded?.taskId).toBe("overstory-abc1");
44
44
  expect(loaded?.sessionId).toBe("session-001");
45
45
  expect(loaded?.progressSummary).toBe("Implemented checkpoint module");
46
46
  expect(loaded?.filesModified).toEqual(["src/agents/checkpoint.ts"]);
@@ -8,6 +8,7 @@ import {
8
8
  buildBashPathBoundaryScript,
9
9
  buildPathBoundaryGuardScript,
10
10
  deployHooks,
11
+ escapeForSingleQuotedShell,
11
12
  getBashPathBoundaryGuards,
12
13
  getCapabilityGuards,
13
14
  getDangerGuards,
@@ -894,35 +895,37 @@ describe("isOverstoryHookEntry", () => {
894
895
  describe("getCapabilityGuards", () => {
895
896
  // 10 native team tool blocks apply to ALL capabilities
896
897
  const NATIVE_TEAM_TOOL_COUNT = 10;
898
+ // 3 interactive tool blocks (AskUserQuestion, EnterPlanMode, EnterWorktree) apply to ALL capabilities
899
+ const INTERACTIVE_TOOL_COUNT = 3;
897
900
 
898
- test("returns 14 guards for scout (10 team + 3 tool blocks + 1 bash file guard)", () => {
901
+ test("returns 17 guards for scout (10 team + 3 interactive + 3 tool blocks + 1 bash file guard)", () => {
899
902
  const guards = getCapabilityGuards("scout");
900
- expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 4);
903
+ expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 4);
901
904
  });
902
905
 
903
- test("returns 14 guards for reviewer (10 team + 3 tool blocks + 1 bash file guard)", () => {
906
+ test("returns 17 guards for reviewer (10 team + 3 interactive + 3 tool blocks + 1 bash file guard)", () => {
904
907
  const guards = getCapabilityGuards("reviewer");
905
- expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 4);
908
+ expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 4);
906
909
  });
907
910
 
908
- test("returns 14 guards for lead (10 team + 3 tool blocks + 1 bash file guard)", () => {
911
+ test("returns 17 guards for lead (10 team + 3 interactive + 3 tool blocks + 1 bash file guard)", () => {
909
912
  const guards = getCapabilityGuards("lead");
910
- expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 4);
913
+ expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 4);
911
914
  });
912
915
 
913
- test("returns 11 guards for builder (10 team + 1 bash path boundary)", () => {
916
+ test("returns 14 guards for builder (10 team + 3 interactive + 1 bash path boundary)", () => {
914
917
  const guards = getCapabilityGuards("builder");
915
- expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 1);
918
+ expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 1);
916
919
  });
917
920
 
918
- test("returns 11 guards for merger (10 team + 1 bash path boundary)", () => {
921
+ test("returns 14 guards for merger (10 team + 3 interactive + 1 bash path boundary)", () => {
919
922
  const guards = getCapabilityGuards("merger");
920
- expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 1);
923
+ expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 1);
921
924
  });
922
925
 
923
- test("returns 10 guards for unknown capability (10 team tool blocks only)", () => {
926
+ test("returns 13 guards for unknown capability (10 team + 3 interactive tool blocks)", () => {
924
927
  const guards = getCapabilityGuards("unknown");
925
- expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT);
928
+ expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT);
926
929
  });
927
930
 
928
931
  test("builder gets Bash path boundary guard", () => {
@@ -1039,14 +1042,90 @@ describe("getCapabilityGuards", () => {
1039
1042
  expect(taskGuard?.hooks[0]?.command).toContain('[ -z "$OVERSTORY_AGENT_NAME" ] && exit 0;');
1040
1043
  });
1041
1044
 
1042
- test("coordinator gets 14 guards (10 team + 3 tool blocks + 1 bash file guard)", () => {
1045
+ test("coordinator gets 17 guards (10 team + 3 interactive + 3 tool blocks + 1 bash file guard)", () => {
1043
1046
  const guards = getCapabilityGuards("coordinator");
1044
- expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 4);
1047
+ expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 4);
1045
1048
  });
1046
1049
 
1047
- test("supervisor gets 14 guards (10 team + 3 tool blocks + 1 bash file guard)", () => {
1050
+ test("supervisor gets 17 guards (10 team + 3 interactive + 3 tool blocks + 1 bash file guard)", () => {
1048
1051
  const guards = getCapabilityGuards("supervisor");
1049
- expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + 4);
1052
+ expect(guards.length).toBe(NATIVE_TEAM_TOOL_COUNT + INTERACTIVE_TOOL_COUNT + 4);
1053
+ });
1054
+
1055
+ test("all capabilities get AskUserQuestion blocked", () => {
1056
+ for (const cap of [
1057
+ "scout",
1058
+ "reviewer",
1059
+ "lead",
1060
+ "coordinator",
1061
+ "supervisor",
1062
+ "builder",
1063
+ "merger",
1064
+ "unknown",
1065
+ ]) {
1066
+ const guards = getCapabilityGuards(cap);
1067
+ const guard = guards.find((g) => g.matcher === "AskUserQuestion");
1068
+ expect(guard).toBeDefined();
1069
+ expect(guard?.hooks[0]?.command).toContain("human interaction");
1070
+ expect(guard?.hooks[0]?.command).toContain("overstory mail");
1071
+ }
1072
+ });
1073
+
1074
+ test("all capabilities get EnterPlanMode blocked", () => {
1075
+ for (const cap of [
1076
+ "scout",
1077
+ "reviewer",
1078
+ "lead",
1079
+ "coordinator",
1080
+ "supervisor",
1081
+ "builder",
1082
+ "merger",
1083
+ "unknown",
1084
+ ]) {
1085
+ const guards = getCapabilityGuards(cap);
1086
+ const guard = guards.find((g) => g.matcher === "EnterPlanMode");
1087
+ expect(guard).toBeDefined();
1088
+ expect(guard?.hooks[0]?.command).toContain("human interaction");
1089
+ expect(guard?.hooks[0]?.command).toContain("overstory mail");
1090
+ }
1091
+ });
1092
+
1093
+ test("all capabilities get EnterWorktree blocked", () => {
1094
+ for (const cap of [
1095
+ "scout",
1096
+ "reviewer",
1097
+ "lead",
1098
+ "coordinator",
1099
+ "supervisor",
1100
+ "builder",
1101
+ "merger",
1102
+ "unknown",
1103
+ ]) {
1104
+ const guards = getCapabilityGuards(cap);
1105
+ const guard = guards.find((g) => g.matcher === "EnterWorktree");
1106
+ expect(guard).toBeDefined();
1107
+ expect(guard?.hooks[0]?.command).toContain("human interaction");
1108
+ expect(guard?.hooks[0]?.command).toContain("overstory mail");
1109
+ }
1110
+ });
1111
+
1112
+ test("interactive guards include env var guard prefix", () => {
1113
+ const guards = getCapabilityGuards("builder");
1114
+ for (const tool of ["AskUserQuestion", "EnterPlanMode", "EnterWorktree"]) {
1115
+ const guard = guards.find((g) => g.matcher === tool);
1116
+ expect(guard).toBeDefined();
1117
+ expect(guard?.hooks[0]?.command).toContain('[ -z "$OVERSTORY_AGENT_NAME" ] && exit 0;');
1118
+ }
1119
+ });
1120
+
1121
+ test("interactive guard block reason mentions tool name", () => {
1122
+ const guards = getCapabilityGuards("scout");
1123
+ const askGuard = guards.find((g) => g.matcher === "AskUserQuestion");
1124
+ expect(askGuard?.hooks[0]?.command).toContain("AskUserQuestion");
1125
+ const planGuard = guards.find((g) => g.matcher === "EnterPlanMode");
1126
+ expect(planGuard?.hooks[0]?.command).toContain("EnterPlanMode");
1127
+ const worktreeGuard = guards.find((g) => g.matcher === "EnterWorktree");
1128
+ expect(worktreeGuard?.hooks[0]?.command).toContain("EnterWorktree");
1050
1129
  });
1051
1130
  });
1052
1131
 
@@ -2038,3 +2117,39 @@ describe("bash path boundary integration", () => {
2038
2117
  expect(universalGuard.hooks[0].command).toContain('"decision":"block"');
2039
2118
  });
2040
2119
  });
2120
+
2121
+ describe("escapeForSingleQuotedShell", () => {
2122
+ test("no single quotes: string passes through unchanged", () => {
2123
+ expect(escapeForSingleQuotedShell("hello world")).toBe("hello world");
2124
+ });
2125
+
2126
+ test("single quotes escaped: it's becomes it'\\''s", () => {
2127
+ expect(escapeForSingleQuotedShell("it's")).toBe("it'\\''s");
2128
+ });
2129
+
2130
+ test("multiple single quotes: each one is escaped independently", () => {
2131
+ expect(escapeForSingleQuotedShell("can't won't")).toBe("can'\\''t won'\\''t");
2132
+ });
2133
+
2134
+ test("empty string: returns empty string", () => {
2135
+ expect(escapeForSingleQuotedShell("")).toBe("");
2136
+ });
2137
+
2138
+ test("blockGuard shell command outputs valid JSON when executed", async () => {
2139
+ const guards = getCapabilityGuards("builder");
2140
+ const taskGuard = guards.find((g) => g.matcher === "Task");
2141
+ expect(taskGuard).toBeDefined();
2142
+ const cmd = taskGuard?.hooks[0]?.command ?? "";
2143
+ const echoCmd = cmd.replace('[ -z "$OVERSTORY_AGENT_NAME" ] && exit 0; ', "");
2144
+ const proc = Bun.spawn(["sh", "-c", echoCmd], {
2145
+ stdout: "pipe",
2146
+ stderr: "pipe",
2147
+ env: { ...process.env, OVERSTORY_AGENT_NAME: "test-agent" },
2148
+ });
2149
+ const output = await new Response(proc.stdout).text();
2150
+ await proc.exited;
2151
+ const parsed = JSON.parse(output.trim());
2152
+ expect(parsed.decision).toBe("block");
2153
+ expect(parsed.reason).toContain("overstory sling");
2154
+ });
2155
+ });
@@ -46,6 +46,13 @@ const NATIVE_TEAM_TOOLS = [
46
46
  "TaskStop",
47
47
  ];
48
48
 
49
+ /**
50
+ * Tools that require human interaction and block indefinitely in non-interactive
51
+ * tmux sessions. Agents run non-interactively and must never call these tools.
52
+ * Use overstory mail (--type question) to escalate to the orchestrator instead.
53
+ */
54
+ const INTERACTIVE_TOOLS = ["AskUserQuestion", "EnterPlanMode", "EnterWorktree"];
55
+
49
56
  /** Tools that non-implementation agents must not use. */
50
57
  const WRITE_TOOLS = ["Write", "Edit", "NotebookEdit"];
51
58
 
@@ -198,6 +205,20 @@ export function getPathBoundaryGuards(): HookEntry[] {
198
205
  ];
199
206
  }
200
207
 
208
+ /**
209
+ * Escape a string for use inside a single-quoted POSIX shell string.
210
+ *
211
+ * POSIX single-quoted strings cannot contain single quotes at all.
212
+ * The standard technique is to end the single-quoted segment, emit an escaped
213
+ * single quote using $'\'', then start a new single-quoted segment:
214
+ * 'it'\''s fine' → it's fine
215
+ *
216
+ * Exported so tests can verify escaping directly.
217
+ */
218
+ export function escapeForSingleQuotedShell(str: string): string {
219
+ return str.replace(/'/g, "'\\''");
220
+ }
221
+
201
222
  /**
202
223
  * Build a PreToolUse guard that blocks a specific tool.
203
224
  *
@@ -211,7 +232,7 @@ function blockGuard(toolName: string, reason: string): HookEntry {
211
232
  hooks: [
212
233
  {
213
234
  type: "command",
214
- command: `${ENV_GUARD} echo '${response}'`,
235
+ command: `${ENV_GUARD} echo '${escapeForSingleQuotedShell(response)}'`,
215
236
  },
216
237
  ],
217
238
  };
@@ -445,6 +466,17 @@ export function getCapabilityGuards(capability: string): HookEntry[] {
445
466
  );
446
467
  guards.push(...teamToolGuards);
447
468
 
469
+ // Block interactive tools for ALL overstory agents.
470
+ // These tools require a human to respond and block indefinitely in tmux sessions.
471
+ // Agents must use overstory mail (--type question) to escalate instead.
472
+ const interactiveGuards = INTERACTIVE_TOOLS.map((tool) =>
473
+ blockGuard(
474
+ tool,
475
+ `${tool} requires human interaction -- agents run non-interactively. Use overstory mail (--type question) to escalate`,
476
+ ),
477
+ );
478
+ guards.push(...interactiveGuards);
479
+
448
480
  if (NON_IMPLEMENTATION_CAPABILITIES.has(capability)) {
449
481
  const toolGuards = WRITE_TOOLS.map((tool) =>
450
482
  blockGuard(tool, `${capability} agents cannot modify files — ${tool} is not allowed`),
@@ -72,12 +72,12 @@ describe("identity", () => {
72
72
  expertiseDomains: [],
73
73
  recentTasks: [
74
74
  {
75
- beadId: "beads-001",
75
+ taskId: "beads-001",
76
76
  summary: "Fixed authentication bug",
77
77
  completedAt: "2024-01-15T12:00:00Z",
78
78
  },
79
79
  {
80
- beadId: "beads-002",
80
+ taskId: "beads-002",
81
81
  summary: "Added user profile page",
82
82
  completedAt: "2024-01-16T14:30:00Z",
83
83
  },
@@ -89,10 +89,10 @@ describe("identity", () => {
89
89
  const filePath = join(tempDir, "test-agent", "identity.yaml");
90
90
  const content = await Bun.file(filePath).text();
91
91
  expect(content).toContain("recentTasks:");
92
- expect(content).toContain("\t- beadId: beads-001");
92
+ expect(content).toContain("\t- taskId: beads-001");
93
93
  expect(content).toContain("\t\tsummary: Fixed authentication bug");
94
94
  expect(content).toContain('\t\tcompletedAt: "2024-01-15T12:00:00Z"');
95
- expect(content).toContain("\t- beadId: beads-002");
95
+ expect(content).toContain("\t- taskId: beads-002");
96
96
  expect(content).toContain("\t\tsummary: Added user profile page");
97
97
  expect(content).toContain('\t\tcompletedAt: "2024-01-16T14:30:00Z"');
98
98
  });
@@ -106,7 +106,7 @@ describe("identity", () => {
106
106
  expertiseDomains: ["domain: with colon", "domain#with hash", " leading space"],
107
107
  recentTasks: [
108
108
  {
109
- beadId: "beads-001",
109
+ taskId: "beads-001",
110
110
  summary: 'Fixed bug: "memory leak"',
111
111
  completedAt: "2024-01-15T12:00:00Z",
112
112
  },
@@ -198,7 +198,7 @@ describe("identity", () => {
198
198
  expertiseDomains: ["typescript", "testing"],
199
199
  recentTasks: [
200
200
  {
201
- beadId: "beads-001",
201
+ taskId: "beads-001",
202
202
  summary: "Fixed bug",
203
203
  completedAt: "2024-01-15T12:00:00Z",
204
204
  },
@@ -216,7 +216,7 @@ describe("identity", () => {
216
216
  expect(loaded?.sessionsCompleted).toBe(7);
217
217
  expect(loaded?.expertiseDomains).toEqual(["typescript", "testing"]);
218
218
  expect(loaded?.recentTasks).toHaveLength(1);
219
- expect(loaded?.recentTasks[0]?.beadId).toBe("beads-001");
219
+ expect(loaded?.recentTasks[0]?.taskId).toBe("beads-001");
220
220
  expect(loaded?.recentTasks[0]?.summary).toBe("Fixed bug");
221
221
  expect(loaded?.recentTasks[0]?.completedAt).toBe("2024-01-15T12:00:00Z");
222
222
  });
@@ -252,17 +252,17 @@ describe("identity", () => {
252
252
  expertiseDomains: [],
253
253
  recentTasks: [
254
254
  {
255
- beadId: "beads-001",
255
+ taskId: "beads-001",
256
256
  summary: "Task 1",
257
257
  completedAt: "2024-01-15T12:00:00Z",
258
258
  },
259
259
  {
260
- beadId: "beads-002",
260
+ taskId: "beads-002",
261
261
  summary: "Task 2",
262
262
  completedAt: "2024-01-16T12:00:00Z",
263
263
  },
264
264
  {
265
- beadId: "beads-003",
265
+ taskId: "beads-003",
266
266
  summary: "Task 3",
267
267
  completedAt: "2024-01-17T12:00:00Z",
268
268
  },
@@ -273,9 +273,9 @@ describe("identity", () => {
273
273
  const loaded = await loadIdentity(tempDir, "test-agent");
274
274
 
275
275
  expect(loaded?.recentTasks).toHaveLength(3);
276
- expect(loaded?.recentTasks[0]?.beadId).toBe("beads-001");
277
- expect(loaded?.recentTasks[1]?.beadId).toBe("beads-002");
278
- expect(loaded?.recentTasks[2]?.beadId).toBe("beads-003");
276
+ expect(loaded?.recentTasks[0]?.taskId).toBe("beads-001");
277
+ expect(loaded?.recentTasks[1]?.taskId).toBe("beads-002");
278
+ expect(loaded?.recentTasks[2]?.taskId).toBe("beads-003");
279
279
  });
280
280
 
281
281
  test("handles quoted strings with special characters", async () => {
@@ -287,7 +287,7 @@ describe("identity", () => {
287
287
  expertiseDomains: ["domain: with colon", "domain#with hash"],
288
288
  recentTasks: [
289
289
  {
290
- beadId: "beads-001",
290
+ taskId: "beads-001",
291
291
  summary: 'Fixed bug: "memory leak"',
292
292
  completedAt: "2024-01-15T12:00:00Z",
293
293
  },
@@ -311,7 +311,7 @@ describe("identity", () => {
311
311
  expertiseDomains: [],
312
312
  recentTasks: [
313
313
  {
314
- beadId: "beads-001",
314
+ taskId: "beads-001",
315
315
  summary: "Path: C:\\Users\\test\\file.txt",
316
316
  completedAt: "2024-01-15T12:00:00Z",
317
317
  },
@@ -435,14 +435,14 @@ recentTasks: []
435
435
  const beforeUpdate = Date.now();
436
436
  const updated = await updateIdentity(tempDir, "test-agent", {
437
437
  completedTask: {
438
- beadId: "beads-001",
438
+ taskId: "beads-001",
439
439
  summary: "Fixed authentication bug",
440
440
  },
441
441
  });
442
442
  const afterUpdate = Date.now();
443
443
 
444
444
  expect(updated.recentTasks).toHaveLength(1);
445
- expect(updated.recentTasks[0]?.beadId).toBe("beads-001");
445
+ expect(updated.recentTasks[0]?.taskId).toBe("beads-001");
446
446
  expect(updated.recentTasks[0]?.summary).toBe("Fixed authentication bug");
447
447
 
448
448
  // Verify timestamp is within the update window
@@ -454,7 +454,7 @@ recentTasks: []
454
454
  test("caps recentTasks at 20 entries, dropping oldest", async () => {
455
455
  // Create identity with 19 tasks
456
456
  const existingTasks = Array.from({ length: 19 }, (_, i) => ({
457
- beadId: `beads-${i.toString().padStart(3, "0")}`,
457
+ taskId: `beads-${i.toString().padStart(3, "0")}`,
458
458
  summary: `Task ${i}`,
459
459
  completedAt: `2024-01-${(i + 1).toString().padStart(2, "0")}T12:00:00Z`,
460
460
  }));
@@ -472,20 +472,20 @@ recentTasks: []
472
472
 
473
473
  // Add two more tasks (total would be 21)
474
474
  let updated = await updateIdentity(tempDir, "test-agent", {
475
- completedTask: { beadId: "beads-019", summary: "Task 19" },
475
+ completedTask: { taskId: "beads-019", summary: "Task 19" },
476
476
  });
477
477
 
478
478
  expect(updated.recentTasks).toHaveLength(20);
479
- expect(updated.recentTasks[0]?.beadId).toBe("beads-000");
479
+ expect(updated.recentTasks[0]?.taskId).toBe("beads-000");
480
480
 
481
481
  updated = await updateIdentity(tempDir, "test-agent", {
482
- completedTask: { beadId: "beads-020", summary: "Task 20" },
482
+ completedTask: { taskId: "beads-020", summary: "Task 20" },
483
483
  });
484
484
 
485
485
  expect(updated.recentTasks).toHaveLength(20);
486
486
  // Oldest task (beads-000) should be dropped
487
- expect(updated.recentTasks[0]?.beadId).toBe("beads-001");
488
- expect(updated.recentTasks[19]?.beadId).toBe("beads-020");
487
+ expect(updated.recentTasks[0]?.taskId).toBe("beads-001");
488
+ expect(updated.recentTasks[19]?.taskId).toBe("beads-020");
489
489
  });
490
490
 
491
491
  test("applies multiple updates simultaneously", async () => {
@@ -503,7 +503,7 @@ recentTasks: []
503
503
  sessionsCompleted: 2,
504
504
  expertiseDomains: ["testing", "architecture"],
505
505
  completedTask: {
506
- beadId: "beads-001",
506
+ taskId: "beads-001",
507
507
  summary: "Completed task",
508
508
  },
509
509
  });
@@ -554,12 +554,12 @@ recentTasks: []
554
554
  expertiseDomains: ["typescript", "testing", "architecture"],
555
555
  recentTasks: [
556
556
  {
557
- beadId: "beads-001",
557
+ taskId: "beads-001",
558
558
  summary: "Implemented feature X",
559
559
  completedAt: "2024-01-15T12:00:00Z",
560
560
  },
561
561
  {
562
- beadId: "beads-002",
562
+ taskId: "beads-002",
563
563
  summary: "Fixed bug in module Y",
564
564
  completedAt: "2024-01-16T14:30:00Z",
565
565
  },
@@ -587,7 +587,7 @@ recentTasks: []
587
587
  ],
588
588
  recentTasks: [
589
589
  {
590
- beadId: "beads-001",
590
+ taskId: "beads-001",
591
591
  summary: 'Summary with "quotes" and: colons',
592
592
  completedAt: "2024-01-15T12:00:00Z",
593
593
  },
@@ -39,7 +39,7 @@ function serializeIdentityYaml(identity: AgentIdentity): string {
39
39
  } else {
40
40
  lines.push("recentTasks:");
41
41
  for (const task of identity.recentTasks) {
42
- lines.push(`\t- beadId: ${quoteIfNeeded(task.beadId)}`);
42
+ lines.push(`\t- taskId: ${quoteIfNeeded(task.taskId)}`);
43
43
  lines.push(`\t\tsummary: ${quoteIfNeeded(task.summary)}`);
44
44
  lines.push(`\t\tcompletedAt: ${quoteIfNeeded(task.completedAt)}`);
45
45
  }
@@ -82,7 +82,7 @@ function quoteIfNeeded(value: string): string {
82
82
  * This is a purpose-built parser for the identity YAML format. It handles:
83
83
  * - Simple key: value pairs (strings, numbers)
84
84
  * - Arrays of scalars (expertiseDomains)
85
- * - Arrays of objects (recentTasks with beadId, summary, completedAt)
85
+ * - Arrays of objects (recentTasks with taskId, summary, completedAt)
86
86
  * - Empty arrays (`[]`)
87
87
  * - Quoted strings
88
88
  * - Tab indentation
@@ -95,10 +95,10 @@ function parseIdentityYaml(text: string): AgentIdentity {
95
95
  let created = "";
96
96
  let sessionsCompleted = 0;
97
97
  const expertiseDomains: string[] = [];
98
- const recentTasks: Array<{ beadId: string; summary: string; completedAt: string }> = [];
98
+ const recentTasks: Array<{ taskId: string; summary: string; completedAt: string }> = [];
99
99
 
100
100
  let currentSection: "none" | "expertiseDomains" | "recentTasks" = "none";
101
- let currentTask: { beadId: string; summary: string; completedAt: string } | null = null;
101
+ let currentTask: { taskId: string; summary: string; completedAt: string } | null = null;
102
102
 
103
103
  for (const rawLine of lines) {
104
104
  const trimmed = rawLine.trim();
@@ -169,7 +169,7 @@ function parseIdentityYaml(text: string): AgentIdentity {
169
169
  if (currentTask !== null) {
170
170
  recentTasks.push(currentTask);
171
171
  }
172
- currentTask = { beadId: "", summary: "", completedAt: "" };
172
+ currentTask = { taskId: "", summary: "", completedAt: "" };
173
173
 
174
174
  // Parse the key-value on the same line as the dash
175
175
  const itemContent = trimmed.slice(2).trim();
@@ -210,13 +210,13 @@ function parseIdentityYaml(text: string): AgentIdentity {
210
210
  * Assign a parsed field value to a task object by key name.
211
211
  */
212
212
  function assignTaskField(
213
- task: { beadId: string; summary: string; completedAt: string },
213
+ task: { taskId: string; summary: string; completedAt: string },
214
214
  key: string,
215
215
  value: string,
216
216
  ): void {
217
217
  switch (key) {
218
- case "beadId":
219
- task.beadId = value;
218
+ case "taskId":
219
+ task.taskId = value;
220
220
  break;
221
221
  case "summary":
222
222
  task.summary = value;
@@ -336,7 +336,7 @@ export async function updateIdentity(
336
336
  baseDir: string,
337
337
  name: string,
338
338
  update: Partial<Pick<AgentIdentity, "sessionsCompleted" | "expertiseDomains">> & {
339
- completedTask?: { beadId: string; summary: string };
339
+ completedTask?: { taskId: string; summary: string };
340
340
  },
341
341
  ): Promise<AgentIdentity> {
342
342
  const identity = await loadIdentity(baseDir, name);
@@ -364,7 +364,7 @@ export async function updateIdentity(
364
364
  // Append completed task
365
365
  if (update.completedTask !== undefined) {
366
366
  identity.recentTasks.push({
367
- beadId: update.completedTask.beadId,
367
+ taskId: update.completedTask.taskId,
368
368
  summary: update.completedTask.summary,
369
369
  completedAt: new Date().toISOString(),
370
370
  });