@os-eco/overstory-cli 0.6.5 → 0.6.6

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 (79) hide show
  1. package/README.md +60 -60
  2. package/agents/builder.md +16 -16
  3. package/agents/coordinator.md +57 -57
  4. package/agents/issue-reviews.md +71 -0
  5. package/agents/lead.md +43 -42
  6. package/agents/merger.md +15 -15
  7. package/agents/monitor.md +37 -37
  8. package/agents/pr-reviews.md +60 -0
  9. package/agents/prioritize.md +110 -0
  10. package/agents/release.md +56 -0
  11. package/agents/reviewer.md +15 -15
  12. package/agents/scout.md +18 -18
  13. package/agents/supervisor.md +78 -78
  14. package/package.json +1 -1
  15. package/src/agents/hooks-deployer.test.ts +23 -26
  16. package/src/agents/hooks-deployer.ts +9 -5
  17. package/src/agents/overlay.test.ts +5 -5
  18. package/src/agents/overlay.ts +11 -11
  19. package/src/commands/agents.ts +7 -6
  20. package/src/commands/clean.ts +5 -5
  21. package/src/commands/completions.test.ts +10 -10
  22. package/src/commands/completions.ts +26 -28
  23. package/src/commands/coordinator.test.ts +2 -2
  24. package/src/commands/coordinator.ts +12 -12
  25. package/src/commands/costs.ts +1 -1
  26. package/src/commands/dashboard.ts +8 -8
  27. package/src/commands/doctor.ts +4 -4
  28. package/src/commands/errors.ts +1 -1
  29. package/src/commands/feed.ts +1 -1
  30. package/src/commands/group.ts +3 -3
  31. package/src/commands/hooks.test.ts +7 -7
  32. package/src/commands/hooks.ts +7 -7
  33. package/src/commands/init.test.ts +6 -2
  34. package/src/commands/init.ts +19 -19
  35. package/src/commands/inspect.ts +19 -19
  36. package/src/commands/log.ts +3 -3
  37. package/src/commands/logs.ts +1 -1
  38. package/src/commands/mail.test.ts +2 -2
  39. package/src/commands/mail.ts +28 -11
  40. package/src/commands/merge.ts +7 -7
  41. package/src/commands/metrics.test.ts +1 -1
  42. package/src/commands/metrics.ts +2 -2
  43. package/src/commands/monitor.test.ts +5 -5
  44. package/src/commands/monitor.ts +4 -4
  45. package/src/commands/nudge.ts +1 -1
  46. package/src/commands/prime.test.ts +1 -1
  47. package/src/commands/prime.ts +2 -2
  48. package/src/commands/replay.ts +1 -1
  49. package/src/commands/run.ts +2 -2
  50. package/src/commands/sling.test.ts +84 -2
  51. package/src/commands/sling.ts +97 -9
  52. package/src/commands/spec.ts +8 -9
  53. package/src/commands/status.test.ts +2 -2
  54. package/src/commands/status.ts +2 -4
  55. package/src/commands/stop.ts +2 -2
  56. package/src/commands/supervisor.test.ts +1 -1
  57. package/src/commands/supervisor.ts +4 -4
  58. package/src/commands/trace.test.ts +2 -2
  59. package/src/commands/trace.ts +4 -4
  60. package/src/commands/watch.ts +5 -5
  61. package/src/commands/worktree.test.ts +3 -3
  62. package/src/commands/worktree.ts +11 -11
  63. package/src/doctor/dependencies.test.ts +5 -5
  64. package/src/doctor/dependencies.ts +2 -2
  65. package/src/doctor/logs.ts +1 -1
  66. package/src/doctor/structure.test.ts +1 -1
  67. package/src/doctor/structure.ts +1 -1
  68. package/src/doctor/version.test.ts +3 -3
  69. package/src/doctor/version.ts +1 -1
  70. package/src/e2e/init-sling-lifecycle.test.ts +6 -2
  71. package/src/index.ts +11 -9
  72. package/src/mail/client.test.ts +1 -1
  73. package/src/mail/client.ts +2 -2
  74. package/src/mulch/client.ts +1 -1
  75. package/src/worktree/tmux.test.ts +8 -3
  76. package/src/worktree/tmux.ts +19 -18
  77. package/templates/CLAUDE.md.tmpl +27 -27
  78. package/templates/hooks.json.tmpl +15 -11
  79. package/templates/overlay.md.tmpl +7 -7
@@ -22,13 +22,13 @@ const SAMPLE_HOOKS = {
22
22
  SessionStart: [
23
23
  {
24
24
  matcher: "",
25
- hooks: [{ type: "command", command: "overstory prime --agent orchestrator" }],
25
+ hooks: [{ type: "command", command: "ov prime --agent orchestrator" }],
26
26
  },
27
27
  ],
28
28
  Stop: [
29
29
  {
30
30
  matcher: "",
31
- hooks: [{ type: "command", command: "overstory log session-end --agent orchestrator" }],
31
+ hooks: [{ type: "command", command: "ov log session-end --agent orchestrator" }],
32
32
  },
33
33
  ],
34
34
  },
@@ -106,7 +106,7 @@ describe("hooks install", () => {
106
106
  const content = await Bun.file(targetPath).text();
107
107
  const parsed = JSON.parse(content) as Record<string, unknown>;
108
108
  expect(parsed.hooks).toBeDefined();
109
- expect(content).toContain("overstory prime");
109
+ expect(content).toContain("ov prime");
110
110
  });
111
111
 
112
112
  test("preserves existing non-hooks keys in settings.local.json", async () => {
@@ -182,7 +182,7 @@ describe("hooks install", () => {
182
182
  // Existing user hook is preserved
183
183
  expect(content).toContain("user-hook");
184
184
  // Overstory hooks are added
185
- expect(content).toContain("overstory prime");
185
+ expect(content).toContain("ov prime");
186
186
  });
187
187
 
188
188
  test("throws when .overstory/hooks.json does not exist", async () => {
@@ -284,7 +284,7 @@ describe("hooks install merge behavior", () => {
284
284
  // User's PreToolUse hook preserved
285
285
  expect(content).toContain("user-write-hook");
286
286
  // Overstory's SessionStart hook added
287
- expect(content).toContain("overstory prime");
287
+ expect(content).toContain("ov prime");
288
288
  // Both event types present
289
289
  expect(parsed.hooks.PreToolUse).toBeDefined();
290
290
  expect(parsed.hooks.SessionStart).toBeDefined();
@@ -361,7 +361,7 @@ describe("hooks install merge behavior", () => {
361
361
  const parsed = JSON.parse(content) as { hooks: Record<string, unknown[]> };
362
362
  expect(parsed.hooks.SessionStart).toBeDefined();
363
363
  expect(parsed.hooks.Stop).toBeDefined();
364
- expect(content).toContain("overstory prime");
364
+ expect(content).toContain("ov prime");
365
365
  });
366
366
 
367
367
  describe("mergeHooksByEventType unit tests", () => {
@@ -424,7 +424,7 @@ describe("hooks status", () => {
424
424
  const output = await captureStdout(() => hooksCommand(["status"]));
425
425
  expect(output).toContain("present");
426
426
  expect(output).toContain("no");
427
- expect(output).toContain("overstory hooks install");
427
+ expect(output).toContain("ov hooks install");
428
428
  });
429
429
 
430
430
  test("reports installed:true when hooks present in .claude/", async () => {
@@ -1,13 +1,13 @@
1
1
  /**
2
- * CLI command: overstory hooks install|uninstall|status
2
+ * CLI command: ov hooks install|uninstall|status
3
3
  *
4
4
  * Manages orchestrator hooks in .claude/settings.local.json.
5
- * Hooks are sourced from .overstory/hooks.json (generated by overstory init).
5
+ * Hooks are sourced from .overstory/hooks.json (generated by ov init).
6
6
  *
7
7
  * This keeps the canonical hook configuration in .overstory/ while placing
8
8
  * a minimal copy in .claude/ only when the user explicitly opts in.
9
- * Running `overstory init` alone does NOT modify .claude/ — the user must
10
- * run `overstory hooks install` as a separate step.
9
+ * Running `ov init` alone does NOT modify .claude/ — the user must
10
+ * run `ov hooks install` as a separate step.
11
11
  */
12
12
 
13
13
  import { mkdir, unlink } from "node:fs/promises";
@@ -80,7 +80,7 @@ async function installHooks(force: boolean): Promise<void> {
80
80
  const sourcePath = join(projectRoot, ".overstory", "hooks.json");
81
81
  const sourceFile = Bun.file(sourcePath);
82
82
  if (!(await sourceFile.exists())) {
83
- throw new ValidationError("No hooks.json found in .overstory/. Run 'overstory init' first.", {
83
+ throw new ValidationError("No hooks.json found in .overstory/. Run 'ov init' first.", {
84
84
  field: "source",
85
85
  });
86
86
  }
@@ -199,7 +199,7 @@ async function statusHooks(json: boolean): Promise<void> {
199
199
  `Hooks installed (.claude/settings.local.json): ${installed ? "yes" : "no"}\n`,
200
200
  );
201
201
  if (!installed && sourceExists) {
202
- process.stdout.write(`\nRun 'overstory hooks install' to install.\n`);
202
+ process.stdout.write(`\nRun 'ov hooks install' to install.\n`);
203
203
  }
204
204
  }
205
205
  }
@@ -234,7 +234,7 @@ export function createHooksCommand(): Command {
234
234
  }
235
235
 
236
236
  /**
237
- * Entry point for `overstory hooks <subcommand>`.
237
+ * Entry point for `ov hooks <subcommand>`.
238
238
  */
239
239
  export async function hooksCommand(args: string[]): Promise<void> {
240
240
  const cmd = createHooksCommand();
@@ -20,6 +20,10 @@ const AGENT_DEF_FILES = [
20
20
  "supervisor.md",
21
21
  "coordinator.md",
22
22
  "monitor.md",
23
+ "issue-reviews.md",
24
+ "pr-reviews.md",
25
+ "prioritize.md",
26
+ "release.md",
23
27
  ];
24
28
 
25
29
  /** Resolve the source agents directory (same logic as init.ts). */
@@ -46,7 +50,7 @@ describe("initCommand: agent-defs deployment", () => {
46
50
  await cleanupTempDir(tempDir);
47
51
  });
48
52
 
49
- test("creates .overstory/agent-defs/ with all 8 agent definition files", async () => {
53
+ test("creates .overstory/agent-defs/ with all 12 agent definition files", async () => {
50
54
  await initCommand({});
51
55
 
52
56
  const agentDefsDir = join(tempDir, ".overstory", "agent-defs");
@@ -100,7 +104,7 @@ describe("initCommand: agent-defs deployment", () => {
100
104
  const stopHooks = parsed.hooks.Stop[0].hooks;
101
105
 
102
106
  expect(stopHooks.length).toBe(2);
103
- expect(stopHooks[0].command).toContain("overstory log session-end");
107
+ expect(stopHooks[0].command).toContain("ov log session-end");
104
108
  expect(stopHooks[1].command).toBe("mulch learn");
105
109
  });
106
110
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * CLI command: overstory init [--force]
2
+ * CLI command: ov init [--force]
3
3
  *
4
4
  * Scaffolds the `.overstory/` directory in the current project with:
5
5
  * - config.yaml (serialized from DEFAULT_CONFIG)
@@ -296,7 +296,7 @@ function buildHooksJson(): string {
296
296
  hooks: [
297
297
  {
298
298
  type: "command",
299
- command: "overstory prime --agent orchestrator",
299
+ command: "ov prime --agent orchestrator",
300
300
  },
301
301
  ],
302
302
  },
@@ -307,7 +307,7 @@ function buildHooksJson(): string {
307
307
  hooks: [
308
308
  {
309
309
  type: "command",
310
- command: "overstory mail check --inject --agent orchestrator",
310
+ command: "ov mail check --inject --agent orchestrator",
311
311
  },
312
312
  ],
313
313
  },
@@ -328,7 +328,7 @@ function buildHooksJson(): string {
328
328
  hooks: [
329
329
  {
330
330
  type: "command",
331
- command: `${toolNameExtract} overstory log tool-start --agent orchestrator --tool-name "$TOOL_NAME"`,
331
+ command: `${toolNameExtract} ov log tool-start --agent orchestrator --tool-name "$TOOL_NAME"`,
332
332
  },
333
333
  ],
334
334
  },
@@ -339,7 +339,7 @@ function buildHooksJson(): string {
339
339
  hooks: [
340
340
  {
341
341
  type: "command",
342
- command: `${toolNameExtract} overstory log tool-end --agent orchestrator --tool-name "$TOOL_NAME"`,
342
+ command: `${toolNameExtract} ov log tool-end --agent orchestrator --tool-name "$TOOL_NAME"`,
343
343
  },
344
344
  ],
345
345
  },
@@ -360,7 +360,7 @@ function buildHooksJson(): string {
360
360
  hooks: [
361
361
  {
362
362
  type: "command",
363
- command: "overstory log session-end --agent orchestrator",
363
+ command: "ov log session-end --agent orchestrator",
364
364
  },
365
365
  {
366
366
  type: "command",
@@ -375,7 +375,7 @@ function buildHooksJson(): string {
375
375
  hooks: [
376
376
  {
377
377
  type: "command",
378
- command: "overstory prime --agent orchestrator --compact",
378
+ command: "ov prime --agent orchestrator --compact",
379
379
  },
380
380
  ],
381
381
  },
@@ -450,11 +450,11 @@ CREATE TABLE IF NOT EXISTS sessions (
450
450
  /**
451
451
  * Content for .overstory/.gitignore — runtime state that should not be tracked.
452
452
  * Uses wildcard+whitelist pattern: ignore everything, whitelist tracked files.
453
- * Auto-healed by overstory prime on each session start.
453
+ * Auto-healed by ov prime on each session start.
454
454
  * Config files (config.yaml, agent-manifest.json, hooks.json) remain tracked.
455
455
  */
456
456
  export const OVERSTORY_GITIGNORE = `# Wildcard+whitelist: ignore everything, whitelist tracked files
457
- # Auto-healed by overstory prime on each session start
457
+ # Auto-healed by ov prime on each session start
458
458
  *
459
459
  !.gitignore
460
460
  !config.yaml
@@ -476,13 +476,13 @@ Overstory turns a single Claude Code session into a multi-agent team by spawning
476
476
 
477
477
  ## Key Commands
478
478
 
479
- - \`overstory init\` — Initialize this directory
480
- - \`overstory status\` — Show active agents and state
481
- - \`overstory sling <id>\` — Spawn a worker agent
482
- - \`overstory mail check\` — Check agent messages
483
- - \`overstory merge\` — Merge agent work back
484
- - \`overstory dashboard\` — Live TUI monitoring
485
- - \`overstory doctor\` — Run health checks
479
+ - \`ov init\` — Initialize this directory
480
+ - \`ov status\` — Show active agents and state
481
+ - \`ov sling <id>\` — Spawn a worker agent
482
+ - \`ov mail check\` — Check agent messages
483
+ - \`ov merge\` — Merge agent work back
484
+ - \`ov dashboard\` — Live TUI monitoring
485
+ - \`ov doctor\` — Run health checks
486
486
 
487
487
  ## Structure
488
488
 
@@ -526,7 +526,7 @@ function printCreated(relativePath: string): void {
526
526
  }
527
527
 
528
528
  /**
529
- * Entry point for `overstory init [--force]`.
529
+ * Entry point for `ov init [--force]`.
530
530
  *
531
531
  * Scaffolds the .overstory/ directory structure in the current working directory.
532
532
  *
@@ -636,6 +636,6 @@ export async function initCommand(opts: InitOptions): Promise<void> {
636
636
  }
637
637
 
638
638
  process.stdout.write("\nDone.\n");
639
- process.stdout.write(" Next: run `overstory hooks install` to enable Claude Code hooks.\n");
640
- process.stdout.write(" Then: run `overstory status` to see the current state.\n");
639
+ process.stdout.write(" Next: run `ov hooks install` to enable Claude Code hooks.\n");
640
+ process.stdout.write(" Then: run `ov status` to see the current state.\n");
641
641
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * CLI command: overstory inspect <agent-name>
2
+ * CLI command: ov inspect <agent-name>
3
3
  *
4
4
  * Deep per-agent inspection aggregating data from EventStore, SessionStore,
5
5
  * MetricsStore, and tmux capture-pane.
@@ -35,15 +35,15 @@ function formatDuration(ms: number): string {
35
35
  function getStateIcon(state: AgentSession["state"]): string {
36
36
  switch (state) {
37
37
  case "booting":
38
- return `${color.yellow("")}`; // Yellow hourglass
38
+ return color.green("-");
39
39
  case "working":
40
- return `${color.green("")}`; // Green circle
40
+ return color.cyan(">");
41
41
  case "stalled":
42
- return `${color.yellow("")}`; // Yellow warning
42
+ return color.yellow("!");
43
43
  case "completed":
44
- return `${color.blue("")}`; // Blue checkmark
44
+ return color.dim("x");
45
45
  case "zombie":
46
- return `${color.red("")}`; // Red skull
46
+ return color.dim("x");
47
47
  default:
48
48
  return "?";
49
49
  }
@@ -252,31 +252,31 @@ export function printInspectData(data: InspectData): void {
252
252
  const w = process.stdout.write.bind(process.stdout);
253
253
  const { session } = data;
254
254
 
255
- w(`\n🔍 Agent Inspection: ${session.agentName}\n`);
255
+ w(`\nAgent Inspection: ${session.agentName}\n`);
256
256
  w(`${"═".repeat(80)}\n\n`);
257
257
 
258
258
  // Agent state and metadata
259
259
  const stateIcon = getStateIcon(session.state);
260
260
  w(`${stateIcon} State: ${session.state}\n`);
261
- w(`⏱ Last activity: ${formatDuration(data.timeSinceLastActivity)} ago\n`);
262
- w(`🎯 Task: ${session.taskId}\n`);
263
- w(`🔧 Capability: ${session.capability}\n`);
264
- w(`🌿 Branch: ${session.branchName}\n`);
261
+ w(`Last activity: ${formatDuration(data.timeSinceLastActivity)} ago\n`);
262
+ w(`Task: ${session.taskId}\n`);
263
+ w(`Capability: ${session.capability}\n`);
264
+ w(`Branch: ${session.branchName}\n`);
265
265
  if (session.parentAgent) {
266
- w(`👤 Parent: ${session.parentAgent} (depth: ${session.depth})\n`);
266
+ w(`Parent: ${session.parentAgent} (depth: ${session.depth})\n`);
267
267
  }
268
- w(`📅 Started: ${session.startedAt}\n`);
269
- w(`💻 Tmux: ${session.tmuxSession}\n`);
268
+ w(`Started: ${session.startedAt}\n`);
269
+ w(`Tmux: ${session.tmuxSession}\n`);
270
270
  w("\n");
271
271
 
272
272
  // Current file
273
273
  if (data.currentFile) {
274
- w(`📝 Current file: ${data.currentFile}\n\n`);
274
+ w(`Current file: ${data.currentFile}\n\n`);
275
275
  }
276
276
 
277
277
  // Token usage
278
278
  if (data.tokenUsage) {
279
- w("💰 Token Usage\n");
279
+ w("Token Usage\n");
280
280
  w(`${"─".repeat(80)}\n`);
281
281
  w(` Input: ${data.tokenUsage.inputTokens.toLocaleString()}\n`);
282
282
  w(` Output: ${data.tokenUsage.outputTokens.toLocaleString()}\n`);
@@ -293,7 +293,7 @@ export function printInspectData(data: InspectData): void {
293
293
 
294
294
  // Tool usage statistics (top 10)
295
295
  if (data.toolStats.length > 0) {
296
- w("🛠 Tool Usage (Top 10)\n");
296
+ w("Tool Usage (Top 10)\n");
297
297
  w(`${"─".repeat(80)}\n`);
298
298
  const top10 = data.toolStats.slice(0, 10);
299
299
  for (const stat of top10) {
@@ -306,7 +306,7 @@ export function printInspectData(data: InspectData): void {
306
306
 
307
307
  // Recent tool calls
308
308
  if (data.recentToolCalls.length > 0) {
309
- w(`📊 Recent Tool Calls (last ${data.recentToolCalls.length})\n`);
309
+ w(`Recent Tool Calls (last ${data.recentToolCalls.length})\n`);
310
310
  w(`${"─".repeat(80)}\n`);
311
311
  for (const call of data.recentToolCalls) {
312
312
  const time = new Date(call.timestamp).toLocaleTimeString();
@@ -322,7 +322,7 @@ export function printInspectData(data: InspectData): void {
322
322
 
323
323
  // tmux output
324
324
  if (data.tmuxOutput) {
325
- w("📺 Live Tmux Output\n");
325
+ w("Live Tmux Output\n");
326
326
  w(`${"─".repeat(80)}\n`);
327
327
  w(`${data.tmuxOutput}\n`);
328
328
  w(`${"─".repeat(80)}\n`);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * CLI command: overstory log <event> --agent <name> [--stdin]
2
+ * CLI command: ov log <event> --agent <name> [--stdin]
3
3
  *
4
4
  * Called by Pre/PostToolUse and Stop hooks.
5
5
  * Events: tool-start, tool-end, session-end.
@@ -542,7 +542,7 @@ async function runLog(opts: {
542
542
  if (agentSession) {
543
543
  // Auto-complete the current run when the coordinator exits.
544
544
  // This handles the case where the user closes the tmux window
545
- // without running `overstory coordinator stop`.
545
+ // without running `ov coordinator stop`.
546
546
  if (agentSession.capability === "coordinator") {
547
547
  try {
548
548
  const currentRunPath = join(config.project.root, ".overstory", "current-run.txt");
@@ -710,7 +710,7 @@ export function createLogCommand(): Command {
710
710
  }
711
711
 
712
712
  /**
713
- * Entry point for `overstory log <event> --agent <name>`.
713
+ * Entry point for `ov log <event> --agent <name>`.
714
714
  */
715
715
  export async function logCommand(args: string[]): Promise<void> {
716
716
  const cmd = createLogCommand();
@@ -1,5 +1,5 @@
1
1
  /**
2
- * CLI command: overstory logs [--agent <name>] [--level <level>] [--since <time>] [--until <time>] [--limit <n>] [--follow] [--json]
2
+ * CLI command: ov logs [--agent <name>] [--level <level>] [--since <time>] [--until <time>] [--limit <n>] [--follow] [--json]
3
3
  *
4
4
  * Queries NDJSON log files from .overstory/logs/{agent-name}/{session-timestamp}/events.ndjson
5
5
  * and presents a unified timeline view.
@@ -1175,7 +1175,7 @@ describe("mailCommand", () => {
1175
1175
  ]);
1176
1176
 
1177
1177
  // Verify warning on stderr
1178
- expect(stderrOutput).toContain("WARNING");
1178
+ expect(stderrOutput).toContain("Warning:");
1179
1179
  expect(stderrOutput).toContain("NO reviewer sessions found");
1180
1180
  expect(stderrOutput).toContain("lead-1");
1181
1181
  expect(stderrOutput).toContain("2 builder(s)");
@@ -1208,7 +1208,7 @@ describe("mailCommand", () => {
1208
1208
  ]);
1209
1209
 
1210
1210
  // Verify note on stderr
1211
- expect(stderrOutput).toContain("NOTE");
1211
+ expect(stderrOutput).toContain("Note:");
1212
1212
  expect(stderrOutput).toContain("Only 1 reviewer(s) for 3 builder(s)");
1213
1213
  expect(stderrOutput).toContain("review-verified");
1214
1214
  });
@@ -373,7 +373,7 @@ async function handleSend(opts: SendOpts, cwd: string): Promise<void> {
373
373
  );
374
374
  } else {
375
375
  process.stdout.write(
376
- `📢 Broadcast sent to ${recipients.length} recipient${recipients.length === 1 ? "" : "s"} (${to})\n`,
376
+ `Broadcast sent to ${recipients.length} recipient${recipients.length === 1 ? "" : "s"} (${to})\n`,
377
377
  );
378
378
  for (let i = 0; i < recipients.length; i++) {
379
379
  const recipient = recipients[i];
@@ -429,7 +429,7 @@ async function handleSend(opts: SendOpts, cwd: string): Promise<void> {
429
429
  if (opts.json) {
430
430
  process.stdout.write(`${JSON.stringify({ id })}\n`);
431
431
  } else {
432
- process.stdout.write(`✉️ Sent message ${id} to ${to}\n`);
432
+ process.stdout.write(`Sent message ${id} to ${to}\n`);
433
433
  }
434
434
 
435
435
  // Auto-nudge: write a pending nudge marker instead of sending tmux keys.
@@ -449,11 +449,28 @@ async function handleSend(opts: SendOpts, cwd: string): Promise<void> {
449
449
  });
450
450
  if (!opts.json) {
451
451
  process.stdout.write(
452
- `📢 Queued nudge for "${to}" (${nudgeReason}, delivered on next prompt)\n`,
452
+ `Queued nudge for "${to}" (${nudgeReason}, delivered on next prompt)\n`,
453
453
  );
454
454
  }
455
455
  }
456
456
 
457
+ // For dispatch messages, also send an immediate tmux nudge.
458
+ // Dispatch targets newly spawned agents that may be idle at the welcome
459
+ // screen where file-based nudges can't reach (no hook fires on idle agents).
460
+ // The I/O corruption concern (overstory-ii1o) only applies during active
461
+ // tool execution — newly spawned agents are idle, so sendKeys is safe.
462
+ if (type === "dispatch") {
463
+ try {
464
+ const { nudgeAgent } = await import("./nudge.ts");
465
+ const nudgeMessage = `[DISPATCH] ${subject}: ${body.slice(0, 500)}`;
466
+ // Small delay to let the agent's TUI stabilize after sling
467
+ await Bun.sleep(3_000);
468
+ await nudgeAgent(cwd, to, nudgeMessage, true); // force=true to skip debounce
469
+ } catch {
470
+ // Non-fatal: the file-based nudge is the fallback
471
+ }
472
+ }
473
+
457
474
  // Reviewer coverage check for merge_ready (advisory warning)
458
475
  if (type === "merge_ready") {
459
476
  try {
@@ -469,13 +486,13 @@ async function handleSend(opts: SendOpts, cwd: string): Promise<void> {
469
486
  );
470
487
  if (myBuilders.length > 0 && myReviewers.length === 0) {
471
488
  process.stderr.write(
472
- `\n⚠️ WARNING: merge_ready sent but NO reviewer sessions found for "${from}".\n` +
473
- `⚠️ ${myBuilders.length} builder(s) completed without review. This violates the review-before-merge requirement.\n` +
474
- `⚠️ Spawn reviewers for each builder before merge. See REVIEW_SKIP in agents/lead.md.\n\n`,
489
+ `\nWarning: merge_ready sent but NO reviewer sessions found for "${from}".\n` +
490
+ `${myBuilders.length} builder(s) completed without review. This violates the review-before-merge requirement.\n` +
491
+ `Spawn reviewers for each builder before merge. See REVIEW_SKIP in agents/lead.md.\n\n`,
475
492
  );
476
493
  } else if (myReviewers.length > 0 && myReviewers.length < myBuilders.length) {
477
494
  process.stderr.write(
478
- `\n⚠️ NOTE: Only ${myReviewers.length} reviewer(s) for ${myBuilders.length} builder(s). Ensure all builder work is review-verified.\n\n`,
495
+ `\nNote: Only ${myReviewers.length} reviewer(s) for ${myBuilders.length} builder(s). Ensure all builder work is review-verified.\n\n`,
479
496
  );
480
497
  }
481
498
  } finally {
@@ -528,7 +545,7 @@ async function handleCheck(opts: CheckOpts, cwd: string): Promise<void> {
528
545
 
529
546
  // Prepend a priority banner if there's a pending nudge
530
547
  if (pendingNudge) {
531
- const banner = `🚨 PRIORITY: ${pendingNudge.reason} message from ${pendingNudge.from} — "${pendingNudge.subject}"\n\n`;
548
+ const banner = `PRIORITY: ${pendingNudge.reason} message from ${pendingNudge.from} — "${pendingNudge.subject}"\n\n`;
532
549
  process.stdout.write(banner);
533
550
  }
534
551
 
@@ -544,7 +561,7 @@ async function handleCheck(opts: CheckOpts, cwd: string): Promise<void> {
544
561
  process.stdout.write("No new messages.\n");
545
562
  } else {
546
563
  process.stdout.write(
547
- `📬 ${messages.length} new message${messages.length === 1 ? "" : "s"}:\n\n`,
564
+ `${messages.length} new message${messages.length === 1 ? "" : "s"}:\n\n`,
548
565
  );
549
566
  for (const msg of messages) {
550
567
  process.stdout.write(`${formatMessage(msg)}\n\n`);
@@ -617,7 +634,7 @@ function handleReply(id: string, opts: ReplyOpts, cwd: string): void {
617
634
  if (opts.json) {
618
635
  process.stdout.write(`${JSON.stringify({ id: replyId })}\n`);
619
636
  } else {
620
- process.stdout.write(`✉️ Reply sent: ${replyId}\n`);
637
+ process.stdout.write(`Reply sent: ${replyId}\n`);
621
638
  }
622
639
  } finally {
623
640
  client.close();
@@ -677,7 +694,7 @@ export async function mailCommand(args: string[]): Promise<void> {
677
694
  const root = await resolveProjectRoot(process.cwd());
678
695
 
679
696
  const program = new Command();
680
- program.name("overstory mail").description("Agent messaging system").exitOverride();
697
+ program.name("ov mail").description("Agent messaging system").exitOverride();
681
698
 
682
699
  program
683
700
  .command("send")
@@ -1,14 +1,14 @@
1
1
  /**
2
- * CLI command: overstory merge
2
+ * CLI command: ov merge
3
3
  *
4
4
  * Merges agent branches back to the canonical branch using
5
5
  * the merge queue and tiered conflict resolver.
6
6
  *
7
7
  * Usage:
8
- * overstory merge --branch <name> Merge a specific branch
9
- * overstory merge --all Merge all pending branches
10
- * overstory merge --dry-run Check for conflicts without merging
11
- * overstory merge --json Output results as JSON
8
+ * ov merge --branch <name> Merge a specific branch
9
+ * ov merge --all Merge all pending branches
10
+ * ov merge --dry-run Check for conflicts without merging
11
+ * ov merge --json Output results as JSON
12
12
  */
13
13
 
14
14
  import { join } from "node:path";
@@ -124,7 +124,7 @@ function formatDryRun(entry: MergeEntry): string {
124
124
  }
125
125
 
126
126
  /**
127
- * Entry point for `overstory merge [flags]`.
127
+ * Entry point for `ov merge [flags]`.
128
128
  *
129
129
  * @param opts - Command options
130
130
  */
@@ -136,7 +136,7 @@ export async function mergeCommand(opts: MergeOptions): Promise<void> {
136
136
  const json = opts.json ?? false;
137
137
 
138
138
  if (!branchName && !all) {
139
- throw new ValidationError("Either --branch <name> or --all is required for overstory merge", {
139
+ throw new ValidationError("Either --branch <name> or --all is required for ov merge", {
140
140
  field: "branch|all",
141
141
  });
142
142
  }
@@ -151,7 +151,7 @@ describe("metricsCommand", () => {
151
151
  const out = output();
152
152
 
153
153
  // Check summary stats
154
- expect(out).toContain("📈 Session Metrics");
154
+ expect(out).toContain("Session Metrics");
155
155
  expect(out).toContain("Total sessions: 3");
156
156
  expect(out).toContain("Completed: 2");
157
157
  expect(out).toContain("Avg duration:");
@@ -1,5 +1,5 @@
1
1
  /**
2
- * CLI command: overstory metrics [--last <n>] [--json]
2
+ * CLI command: ov metrics [--last <n>] [--json]
3
3
  *
4
4
  * Shows metrics summary from SQLite store: session durations, success rates,
5
5
  * merge tier distribution, agent utilization.
@@ -63,7 +63,7 @@ async function executeMetrics(opts: MetricsOpts): Promise<void> {
63
63
  return;
64
64
  }
65
65
 
66
- process.stdout.write("📈 Session Metrics\n");
66
+ process.stdout.write("Session Metrics\n");
67
67
  process.stdout.write(`${"═".repeat(60)}\n\n`);
68
68
 
69
69
  // Summary stats
@@ -39,14 +39,14 @@ describe("buildMonitorBeacon", () => {
39
39
  expect(beacon).toContain("mulch prime");
40
40
  });
41
41
 
42
- test("contains startup instruction: overstory status --json", () => {
42
+ test("contains startup instruction: ov status --json", () => {
43
43
  const beacon = buildMonitorBeacon();
44
- expect(beacon).toContain("overstory status --json");
44
+ expect(beacon).toContain("ov status --json");
45
45
  });
46
46
 
47
- test("contains startup instruction: overstory mail check --agent monitor", () => {
47
+ test("contains startup instruction: ov mail check --agent monitor", () => {
48
48
  const beacon = buildMonitorBeacon();
49
- expect(beacon).toContain("overstory mail check --agent monitor");
49
+ expect(beacon).toContain("ov mail check --agent monitor");
50
50
  });
51
51
 
52
52
  test("contains startup instruction: patrol loop", () => {
@@ -86,7 +86,7 @@ describe("monitorCommand", () => {
86
86
  stdoutSpy.mockRestore();
87
87
  });
88
88
 
89
- test("--help prints help text containing 'overstory monitor'", async () => {
89
+ test("--help prints help text containing 'monitor'", async () => {
90
90
  await monitorCommand(["--help"]);
91
91
  const output = stdoutWrites.join("");
92
92
  expect(output).toContain("monitor");
@@ -1,5 +1,5 @@
1
1
  /**
2
- * CLI command: overstory monitor start|stop|status
2
+ * CLI command: ov monitor start|stop|status
3
3
  *
4
4
  * Manages the persistent Tier 2 monitor agent lifecycle. The monitor runs
5
5
  * at the project root (NOT in a worktree), continuously patrols the agent
@@ -9,7 +9,7 @@
9
9
  * Unlike regular agents spawned by sling, the monitor:
10
10
  * - Has no worktree (operates on the main working tree)
11
11
  * - Has no bead assignment (it monitors, not implements)
12
- * - Has no overlay CLAUDE.md (context comes via overstory status + mail)
12
+ * - Has no overlay CLAUDE.md (context comes via ov status + mail)
13
13
  * - Persists across patrol cycles
14
14
  */
15
15
 
@@ -46,7 +46,7 @@ export function buildMonitorBeacon(): string {
46
46
  const parts = [
47
47
  `[OVERSTORY] ${MONITOR_NAME} (monitor/tier-2) ${timestamp}`,
48
48
  "Depth: 0 | Parent: none | Role: continuous fleet patrol",
49
- `Startup: run mulch prime, check fleet (overstory status --json), check mail (overstory mail check --agent ${MONITOR_NAME}), then begin patrol loop`,
49
+ `Startup: run mulch prime, check fleet (ov status --json), check mail (ov mail check --agent ${MONITOR_NAME}), then begin patrol loop`,
50
50
  ];
51
51
  return parts.join(" — ");
52
52
  }
@@ -356,7 +356,7 @@ export function createMonitorCommand(): Command {
356
356
  }
357
357
 
358
358
  /**
359
- * Entry point for `overstory monitor <subcommand>`.
359
+ * Entry point for `ov monitor <subcommand>`.
360
360
  */
361
361
  export async function monitorCommand(args: string[]): Promise<void> {
362
362
  const cmd = createMonitorCommand();
@@ -283,7 +283,7 @@ export async function nudgeAgent(
283
283
  export async function nudgeCommand(args: string[]): Promise<void> {
284
284
  const program = new Command();
285
285
  program
286
- .name("overstory nudge")
286
+ .name("ov nudge")
287
287
  .description("Send a text nudge to an agent")
288
288
  .argument("<agent-name>", "Name of the agent to nudge")
289
289
  .argument("[message...]", "Text to send (default: check mail prompt)")
@@ -361,7 +361,7 @@ recentTasks: []
361
361
 
362
362
  describe("Gitignore auto-heal", () => {
363
363
  const expectedGitignore = `# Wildcard+whitelist: ignore everything, whitelist tracked files
364
- # Auto-healed by overstory prime on each session start
364
+ # Auto-healed by ov prime on each session start
365
365
  *
366
366
  !.gitignore
367
367
  !config.yaml
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `overstory prime` command.
2
+ * `ov prime` command.
3
3
  *
4
4
  * Loads context for the orchestrator or a specific agent and outputs it
5
5
  * to stdout for injection into Claude Code's context via hooks.
@@ -24,7 +24,7 @@ import { getCurrentSessionName } from "../worktree/tmux.ts";
24
24
  * Wildcard+whitelist pattern: ignore everything except tracked config files.
25
25
  */
26
26
  const OVERSTORY_GITIGNORE = `# Wildcard+whitelist: ignore everything, whitelist tracked files
27
- # Auto-healed by overstory prime on each session start
27
+ # Auto-healed by ov prime on each session start
28
28
  *
29
29
  !.gitignore
30
30
  !config.yaml