@os-eco/overstory-cli 0.6.7 → 0.6.8

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 (44) hide show
  1. package/README.md +3 -2
  2. package/agents/coordinator.md +5 -5
  3. package/agents/lead.md +1 -6
  4. package/agents/merger.md +3 -3
  5. package/agents/reviewer.md +2 -2
  6. package/agents/scout.md +3 -3
  7. package/agents/supervisor.md +16 -16
  8. package/package.json +1 -1
  9. package/src/commands/clean.ts +3 -2
  10. package/src/commands/coordinator.test.ts +7 -12
  11. package/src/commands/coordinator.ts +17 -17
  12. package/src/commands/group.ts +6 -5
  13. package/src/commands/hooks.test.ts +1 -1
  14. package/src/commands/hooks.ts +7 -8
  15. package/src/commands/init.test.ts +1 -5
  16. package/src/commands/init.ts +6 -5
  17. package/src/commands/mail.test.ts +6 -6
  18. package/src/commands/mail.ts +8 -7
  19. package/src/commands/merge.ts +2 -1
  20. package/src/commands/monitor.ts +4 -3
  21. package/src/commands/nudge.ts +2 -1
  22. package/src/commands/run.ts +8 -7
  23. package/src/commands/sling.ts +2 -1
  24. package/src/commands/spec.test.ts +10 -8
  25. package/src/commands/spec.ts +3 -2
  26. package/src/commands/stop.test.ts +8 -6
  27. package/src/commands/stop.ts +3 -2
  28. package/src/commands/supervisor.ts +5 -4
  29. package/src/commands/worktree.test.ts +6 -5
  30. package/src/commands/worktree.ts +19 -26
  31. package/src/doctor/version.ts +2 -2
  32. package/src/e2e/init-sling-lifecycle.test.ts +1 -5
  33. package/src/index.ts +97 -14
  34. package/src/json.test.ts +72 -0
  35. package/src/json.ts +24 -0
  36. package/src/logging/color.test.ts +127 -0
  37. package/src/logging/color.ts +28 -0
  38. package/src/mulch/client.test.ts +22 -22
  39. package/src/worktree/tmux.test.ts +123 -5
  40. package/src/worktree/tmux.ts +38 -8
  41. package/agents/issue-reviews.md +0 -71
  42. package/agents/pr-reviews.md +0 -60
  43. package/agents/prioritize.md +0 -110
  44. package/agents/release.md +0 -56
package/README.md CHANGED
@@ -273,13 +273,13 @@ Global Flags:
273
273
  - **Dependencies**: Minimal runtime — `chalk` (color output), `commander` (CLI framework), core I/O via Bun built-in APIs
274
274
  - **Database**: SQLite via `bun:sqlite` (WAL mode for concurrent access)
275
275
  - **Linting**: Biome (formatter + linter)
276
- - **Testing**: `bun test` (2151 tests across 76 files, colocated with source)
276
+ - **Testing**: `bun test` (2167 tests across 77 files, colocated with source)
277
277
  - **External CLIs**: `bd` (beads) or `sd` (seeds), `mulch`, `git`, `tmux` — invoked as subprocesses
278
278
 
279
279
  ## Development
280
280
 
281
281
  ```bash
282
- # Run tests (2151 tests across 76 files)
282
+ # Run tests (2167 tests across 77 files)
283
283
  bun test
284
284
 
285
285
  # Run a single test
@@ -319,6 +319,7 @@ overstory/
319
319
  types.ts Shared types and interfaces
320
320
  config.ts Config loader + validation
321
321
  errors.ts Custom error types
322
+ json.ts Standardized JSON envelope helpers
322
323
  commands/ One file per CLI subcommand (30 commands)
323
324
  agents.ts Agent discovery and querying
324
325
  coordinator.ts Persistent orchestrator lifecycle
@@ -128,15 +128,15 @@ Coordinator (you, depth 0)
128
128
  - **Your agent name** is `coordinator` (or as set by `$OVERSTORY_AGENT_NAME`)
129
129
 
130
130
  #### Mail Types You Send
131
- - `dispatch` -- assign a work stream to a lead (includes taskId, objective, file area)
131
+ - `dispatch` -- assign a work stream to a lead (includes beadId, objective, file area)
132
132
  - `status` -- progress updates, clarifications, answers to questions
133
133
  - `error` -- report unrecoverable failures to the human operator
134
134
 
135
135
  #### Mail Types You Receive
136
- - `merge_ready` -- lead confirms all builders are done, branch verified and ready to merge (branch, taskId, agentName, filesModified)
137
- - `merged` -- merger confirms successful merge (branch, taskId, tier)
138
- - `merge_failed` -- merger reports merge failure (branch, taskId, conflictFiles, errorMessage)
139
- - `escalation` -- any agent escalates an issue (severity: warning|error|critical, taskId, context)
136
+ - `merge_ready` -- lead confirms all builders are done, branch verified and ready to merge (branch, beadId, agentName, filesModified)
137
+ - `merged` -- merger confirms successful merge (branch, beadId, tier)
138
+ - `merge_failed` -- merger reports merge failure (branch, beadId, conflictFiles, errorMessage)
139
+ - `escalation` -- any agent escalates an issue (severity: warning|error|critical, beadId, context)
140
140
  - `health_check` -- watchdog probes liveness (agentName, checkType)
141
141
  - `status` -- leads report progress
142
142
  - `result` -- leads report completed work streams
package/agents/lead.md CHANGED
@@ -81,7 +81,6 @@ You are primarily a coordinator, but you can also be a doer for simple tasks. Yo
81
81
  - `{{TRACKER_CLI}} sync` (sync {{TRACKER_NAME}} with git)
82
82
  - `ml prime`, `ml record`, `ml query`, `ml search` (expertise)
83
83
  - `ov sling` (spawn sub-workers)
84
- - `ov spec write <id> --body "..." --agent $OVERSTORY_AGENT_NAME` (write spec files)
85
84
  - `ov status` (monitor active agents)
86
85
  - `ov mail send`, `ov mail check`, `ov mail list`, `ov mail read`, `ov mail reply` (communication)
87
86
  - `ov nudge <agent> [message]` (poke stalled workers)
@@ -192,11 +191,7 @@ Delegate exploration to scouts so you can focus on decomposition and planning.
192
191
 
193
192
  Write specs from scout findings and dispatch builders.
194
193
 
195
- 6. **Write spec files** for each subtask based on scout findings using `ov spec write`:
196
- ```bash
197
- ov spec write <subtask-id> --body "<spec content>" --agent $OVERSTORY_AGENT_NAME
198
- ```
199
- Specs are written to `.overstory/specs/<subtask-id>.md` at the canonical root. Each spec should include:
194
+ 6. **Write spec files** for each subtask based on scout findings. Each spec goes to `.overstory/specs/<bead-id>.md` and should include:
200
195
  - Objective (what to build)
201
196
  - Acceptance criteria (how to know it is done)
202
197
  - File scope (which files the builder owns -- non-overlapping)
package/agents/merger.md CHANGED
@@ -19,7 +19,7 @@ These are named failures. If you catch yourself doing any of these, stop and cor
19
19
 
20
20
  ## overlay
21
21
 
22
- Your task-specific context (task ID, branches to merge, target branch, merge order, parent agent) is in `.claude/CLAUDE.md` in your worktree. That file is generated by `ov sling` and tells you WHAT to merge. This file tells you HOW to merge.
22
+ Your task-specific context (task ID, branches to merge, target branch, merge order, parent agent) is in `.claude/CLAUDE.md` in your worktree. That file is generated by `overstory sling` and tells you WHAT to merge. This file tells you HOW to merge.
23
23
 
24
24
  ## constraints
25
25
 
@@ -85,7 +85,7 @@ You are a branch integration specialist. When workers complete their tasks on se
85
85
  - `bun run typecheck` (verify no TypeScript errors)
86
86
  - `{{TRACKER_CLI}} show`, `{{TRACKER_CLI}} close` ({{TRACKER_NAME}} task management)
87
87
  - `ml prime`, `ml query` (load expertise for conflict understanding)
88
- - `ov merge` (use ov merge infrastructure)
88
+ - `ov merge` (use overstory merge infrastructure)
89
89
  - `ov mail send`, `ov mail check` (communication)
90
90
  - `ov status` (check which branches are ready to merge)
91
91
 
@@ -146,7 +146,7 @@ If AI-resolve fails or produces broken code:
146
146
  ```
147
147
  7. **Send detailed merge report** via mail:
148
148
  ```bash
149
- ov mail send --to <parent-or-coordinator> \
149
+ ov mail send --to <parent-or-orchestrator> \
150
150
  --subject "Merge complete: <branch>" \
151
151
  --body "Tier: <tier-used>. Conflicts: <list or none>. Tests: passing." \
152
152
  --type result
@@ -16,7 +16,7 @@ These are named failures. If you catch yourself doing any of these, stop and cor
16
16
 
17
17
  ## overlay
18
18
 
19
- Your task-specific context (task ID, code to review, branch name, parent agent) is in `.claude/CLAUDE.md` in your worktree. That file is generated by `ov sling` and tells you WHAT to review. This file tells you HOW to review.
19
+ Your task-specific context (task ID, code to review, branch name, parent agent) is in `.claude/CLAUDE.md` in your worktree. That file is generated by `overstory sling` and tells you WHAT to review. This file tells you HOW to review.
20
20
 
21
21
  ## constraints
22
22
 
@@ -53,7 +53,7 @@ The only write exception is `ov spec write` for persisting spec files (scout onl
53
53
 
54
54
  1. Verify you have answered the research question or explored the target thoroughly.
55
55
  2. If you produced a spec or detailed report, write it to file: `ov spec write <bead-id> --body "..." --agent <your-name>`.
56
- 3. **Include notable findings in your result mail** — patterns discovered, conventions observed, gotchas encountered. Your parent may record these via ml.
56
+ 3. **Include notable findings in your result mail** — patterns discovered, conventions observed, gotchas encountered. Your parent may record these via mulch.
57
57
  4. Send a SHORT `result` mail to your parent with a concise summary, the spec file path (if applicable), and any notable findings.
58
58
  5. Run `{{TRACKER_CLI}} close <task-id> --reason "<summary of findings>"`.
59
59
  6. Stop. Do not continue exploring after closing.
package/agents/scout.md CHANGED
@@ -16,7 +16,7 @@ These are named failures. If you catch yourself doing any of these, stop and cor
16
16
 
17
17
  ## overlay
18
18
 
19
- Your task-specific context (what to explore, who spawned you, your agent name) is in `.claude/CLAUDE.md` in your worktree. That file is generated by `ov sling` and tells you WHAT to work on. This file tells you HOW to work.
19
+ Your task-specific context (what to explore, who spawned you, your agent name) is in `.claude/CLAUDE.md` in your worktree. That file is generated by `overstory sling` and tells you WHAT to work on. This file tells you HOW to work.
20
20
 
21
21
  ## constraints
22
22
 
@@ -53,7 +53,7 @@ The only write exception is `ov spec write` for persisting spec files (scout onl
53
53
 
54
54
  1. Verify you have answered the research question or explored the target thoroughly.
55
55
  2. If you produced a spec or detailed report, write it to file: `ov spec write <bead-id> --body "..." --agent <your-name>`.
56
- 3. **Include notable findings in your result mail** — patterns discovered, conventions observed, gotchas encountered. Your parent may record these via ml.
56
+ 3. **Include notable findings in your result mail** — patterns discovered, conventions observed, gotchas encountered. Your parent may record these via mulch.
57
57
  4. Send a SHORT `result` mail to your parent with a concise summary, the spec file path (if applicable), and any notable findings.
58
58
  5. Run `{{TRACKER_CLI}} close <task-id> --reason "<summary of findings>"`.
59
59
  6. Stop. Do not continue exploring after closing.
@@ -110,7 +110,7 @@ You perform reconnaissance. Given a research question, exploration target, or an
110
110
  This writes the spec to `.overstory/specs/<bead-id>.md`. Do NOT send full specs via mail.
111
111
  6. **Notify via short mail** after writing a spec file:
112
112
  ```bash
113
- ov mail send --to <parent-or-coordinator> \
113
+ ov mail send --to <parent-or-orchestrator> \
114
114
  --subject "Spec ready: <bead-id>" \
115
115
  --body "Spec written to .overstory/specs/<bead-id>.md — <one-line summary>" \
116
116
  --type result
@@ -133,18 +133,18 @@ Before spawning, check `ov status` to ensure non-overlapping file scope across a
133
133
  - **Read message:** `ov mail read <id> --agent $OVERSTORY_AGENT_NAME`
134
134
 
135
135
  #### Mail Types You Send
136
- - `assign` -- assign work to a specific worker (taskId, specPath, workerName, branch)
137
- - `merge_ready` -- signal to coordinator that a branch is verified and ready for merge (branch, taskId, agentName, filesModified)
136
+ - `assign` -- assign work to a specific worker (beadId, specPath, workerName, branch)
137
+ - `merge_ready` -- signal to coordinator that a branch is verified and ready for merge (branch, beadId, agentName, filesModified)
138
138
  - `status` -- progress updates to coordinator
139
- - `escalation` -- report unresolvable issues to coordinator (severity: warning|error|critical, taskId, context)
139
+ - `escalation` -- report unresolvable issues to coordinator (severity: warning|error|critical, beadId, context)
140
140
  - `question` -- ask coordinator for clarification
141
141
  - `result` -- report completed batch results to coordinator
142
142
 
143
143
  #### Mail Types You Receive
144
- - `dispatch` -- coordinator assigns a task batch (taskId, specPath, capability, fileScope)
145
- - `worker_done` -- worker signals completion (taskId, branch, exitCode, filesModified)
146
- - `merged` -- merger confirms successful merge (branch, taskId, tier)
147
- - `merge_failed` -- merger reports merge failure (branch, taskId, conflictFiles, errorMessage)
144
+ - `dispatch` -- coordinator assigns a task batch (beadId, specPath, capability, fileScope)
145
+ - `worker_done` -- worker signals completion (beadId, branch, exitCode, filesModified)
146
+ - `merged` -- merger confirms successful merge (branch, beadId, tier)
147
+ - `merge_failed` -- merger reports merge failure (branch, beadId, conflictFiles, errorMessage)
148
148
  - `status` -- workers report progress
149
149
  - `question` -- workers ask for clarification
150
150
  - `error` -- workers report failures
@@ -218,7 +218,7 @@ This is your core responsibility. You manage the full worker lifecycle from spaw
218
218
 
219
219
  ### On `worker_done` Received
220
220
 
221
- When a worker sends `worker_done` mail (taskId, branch, exitCode, filesModified):
221
+ When a worker sends `worker_done` mail (beadId, branch, exitCode, filesModified):
222
222
 
223
223
  1. **Verify the branch has commits:**
224
224
  ```bash
@@ -228,7 +228,7 @@ When a worker sends `worker_done` mail (taskId, branch, exitCode, filesModified)
228
228
 
229
229
  2. **Check if the worker closed its bead issue:**
230
230
  ```bash
231
- {{TRACKER_CLI}} show <task-id>
231
+ {{TRACKER_CLI}} show <bead-id>
232
232
  ```
233
233
  Status should be `closed`. If still `open` or `in_progress`, send mail to worker to close it.
234
234
 
@@ -240,17 +240,17 @@ When a worker sends `worker_done` mail (taskId, branch, exitCode, filesModified)
240
240
  --body "Branch <branch> verified for bead <bead-id>. Worker <worker-name> completed successfully." \
241
241
  --type merge_ready --agent $OVERSTORY_AGENT_NAME
242
242
  ```
243
- Include payload: `{"branch": "<branch>", "taskId": "<task-id>", "agentName": "<worker-name>", "filesModified": [...]}`
243
+ Include payload: `{"branch": "<branch>", "beadId": "<bead-id>", "agentName": "<worker-name>", "filesModified": [...]}`
244
244
 
245
245
  5. **If branch has issues,** send mail to worker with `--type error` requesting fixes. Track retry count. After 2 failed attempts, escalate to coordinator.
246
246
 
247
247
  ### On `merged` Received
248
248
 
249
- When coordinator or merger sends `merged` mail (branch, taskId, tier):
249
+ When coordinator or merger sends `merged` mail (branch, beadId, tier):
250
250
 
251
251
  1. **Mark the corresponding bead issue as closed** (if not already):
252
252
  ```bash
253
- {{TRACKER_CLI}} close <task-id> --reason "Merged to main via tier <tier>"
253
+ {{TRACKER_CLI}} close <bead-id> --reason "Merged to main via tier <tier>"
254
254
  ```
255
255
 
256
256
  2. **Clean up worktree:**
@@ -266,7 +266,7 @@ When coordinator or merger sends `merged` mail (branch, taskId, tier):
266
266
 
267
267
  ### On `merge_failed` Received
268
268
 
269
- When merger sends `merge_failed` mail (branch, taskId, conflictFiles, errorMessage):
269
+ When merger sends `merge_failed` mail (branch, beadId, conflictFiles, errorMessage):
270
270
 
271
271
  1. **Assess the failure.** Read `conflictFiles` and `errorMessage` to understand root cause.
272
272
 
@@ -354,7 +354,7 @@ ov mail send --to coordinator --subject "Warning: <brief-description>" \
354
354
  --body "<context and current state>" \
355
355
  --type escalation --priority normal --agent $OVERSTORY_AGENT_NAME
356
356
  ```
357
- Payload: `{"severity": "warning", "taskId": "<task-id>", "context": "<details>"}`
357
+ Payload: `{"severity": "warning", "beadId": "<bead-id>", "context": "<details>"}`
358
358
 
359
359
  #### Error
360
360
  Use when the issue is blocking but recoverable with coordinator intervention:
@@ -368,7 +368,7 @@ ov mail send --to coordinator --subject "Error: <brief-description>" \
368
368
  --body "<what failed, what was tried, what is needed>" \
369
369
  --type escalation --priority high --agent $OVERSTORY_AGENT_NAME
370
370
  ```
371
- Payload: `{"severity": "error", "taskId": "<task-id>", "context": "<detailed-context>"}`
371
+ Payload: `{"severity": "error", "beadId": "<bead-id>", "context": "<detailed-context>"}`
372
372
 
373
373
  #### Critical
374
374
  Use when the automated system cannot self-heal and human intervention is required:
@@ -382,7 +382,7 @@ ov mail send --to coordinator --subject "CRITICAL: <brief-description>" \
382
382
  --body "<what broke, impact scope, manual intervention needed>" \
383
383
  --type escalation --priority urgent --agent $OVERSTORY_AGENT_NAME
384
384
  ```
385
- Payload: `{"severity": "critical", "taskId": null, "context": "<full-details>"}`
385
+ Payload: `{"severity": "critical", "beadId": null, "context": "<full-details>"}`
386
386
 
387
387
  After sending a critical escalation, **stop dispatching new work** for the affected area until the coordinator responds.
388
388
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@os-eco/overstory-cli",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
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",
@@ -25,6 +25,7 @@ import { join } from "node:path";
25
25
  import { loadConfig } from "../config.ts";
26
26
  import { ValidationError } from "../errors.ts";
27
27
  import { createEventStore } from "../events/store.ts";
28
+ import { printHint, printSuccess } from "../logging/color.ts";
28
29
  import { createMulchClient } from "../mulch/client.ts";
29
30
  import { openSessionStore } from "../sessions/compat.ts";
30
31
  import type { AgentSession, MulchDoctorResult, MulchPruneResult, MulchStatus } from "../types.ts";
@@ -584,7 +585,7 @@ export async function cleanCommand(opts: CleanOptions): Promise<void> {
584
585
  }
585
586
 
586
587
  if (lines.length === 0) {
587
- process.stdout.write("Nothing to clean.\n");
588
+ printHint("Nothing to clean");
588
589
  } else {
589
590
  if (result.mulchHealth?.checked) {
590
591
  process.stdout.write("\n--- Cleanup Results ---\n");
@@ -592,6 +593,6 @@ export async function cleanCommand(opts: CleanOptions): Promise<void> {
592
593
  for (const line of lines) {
593
594
  process.stdout.write(`${line}\n`);
594
595
  }
595
- process.stdout.write("\nClean complete.\n");
596
+ printSuccess("Clean complete");
596
597
  }
597
598
  }
@@ -1185,7 +1185,7 @@ describe("watchdog integration", () => {
1185
1185
  Bun.sleep = originalSleep;
1186
1186
  }
1187
1187
 
1188
- expect(output).toContain("Watchdog: started (PID 88888)");
1188
+ expect(output).toContain("Watchdog started");
1189
1189
  });
1190
1190
  });
1191
1191
 
@@ -1414,7 +1414,7 @@ describe("monitor integration", () => {
1414
1414
  Bun.sleep = originalSleep;
1415
1415
  }
1416
1416
 
1417
- expect(output).toContain("Monitor: started (PID 77777)");
1417
+ expect(output).toContain("Monitor started");
1418
1418
  });
1419
1419
 
1420
1420
  test("does NOT call monitor.start() when tier2Enabled is false", async () => {
@@ -1460,21 +1460,16 @@ describe("monitor integration", () => {
1460
1460
  const originalSleep = Bun.sleep;
1461
1461
  Bun.sleep = (() => Promise.resolve()) as typeof Bun.sleep;
1462
1462
 
1463
- let stderrOutput = "";
1464
- const origStderrWrite = process.stderr.write.bind(process.stderr);
1465
- process.stderr.write = (chunk: string | Uint8Array) => {
1466
- stderrOutput += typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
1467
- return true;
1468
- };
1469
-
1463
+ let output: string;
1470
1464
  try {
1471
- await captureStdout(() => coordinatorCommand(["start", "--monitor", "--no-attach"], deps));
1465
+ output = await captureStdout(() =>
1466
+ coordinatorCommand(["start", "--monitor", "--no-attach"], deps),
1467
+ );
1472
1468
  } finally {
1473
1469
  Bun.sleep = originalSleep;
1474
- process.stderr.write = origStderrWrite;
1475
1470
  }
1476
1471
 
1477
- expect(stderrOutput).toContain("skipped");
1472
+ expect(output).toContain("skipped");
1478
1473
  });
1479
1474
  });
1480
1475
 
@@ -20,6 +20,7 @@ import { createIdentity, loadIdentity } from "../agents/identity.ts";
20
20
  import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
21
21
  import { loadConfig } from "../config.ts";
22
22
  import { AgentError, ValidationError } from "../errors.ts";
23
+ import { printHint, printSuccess, printWarning } from "../logging/color.ts";
23
24
  import { openSessionStore } from "../sessions/compat.ts";
24
25
  import { createRunStore } from "../sessions/store.ts";
25
26
  import { resolveBackend, trackerCliName } from "../tracker/factory.ts";
@@ -428,9 +429,9 @@ async function startCoordinator(
428
429
  const watchdogResult = await watchdog.start();
429
430
  if (watchdogResult) {
430
431
  watchdogPid = watchdogResult.pid;
431
- if (!json) process.stdout.write(` Watchdog: started (PID ${watchdogResult.pid})\n`);
432
+ if (!json) printHint("Watchdog started");
432
433
  } else {
433
- if (!json) process.stderr.write(" Watchdog: failed to start or already running\n");
434
+ if (!json) printWarning("Watchdog failed to start");
434
435
  }
435
436
  }
436
437
 
@@ -438,15 +439,14 @@ async function startCoordinator(
438
439
  let monitorPid: number | undefined;
439
440
  if (monitorFlag) {
440
441
  if (!config.watchdog.tier2Enabled) {
441
- if (!json)
442
- process.stderr.write(" Monitor: skipped (watchdog.tier2Enabled is false in config)\n");
442
+ if (!json) printWarning("Monitor skipped", "watchdog.tier2Enabled is false");
443
443
  } else {
444
444
  const monitorResult = await monitor.start([]);
445
445
  if (monitorResult) {
446
446
  monitorPid = monitorResult.pid;
447
- if (!json) process.stdout.write(` Monitor: started (PID ${monitorResult.pid})\n`);
447
+ if (!json) printHint("Monitor started");
448
448
  } else {
449
- if (!json) process.stderr.write(" Monitor: failed to start or already running\n");
449
+ if (!json) printWarning("Monitor failed to start");
450
450
  }
451
451
  }
452
452
  }
@@ -464,7 +464,7 @@ async function startCoordinator(
464
464
  if (json) {
465
465
  process.stdout.write(`${JSON.stringify(output)}\n`);
466
466
  } else {
467
- process.stdout.write("Coordinator started\n");
467
+ printSuccess("Coordinator started");
468
468
  process.stdout.write(` Tmux: ${tmuxSession}\n`);
469
469
  process.stdout.write(` Root: ${projectRoot}\n`);
470
470
  process.stdout.write(` PID: ${pid}\n`);
@@ -568,21 +568,21 @@ async function stopCoordinator(opts: { json: boolean }, deps: CoordinatorDeps =
568
568
  `${JSON.stringify({ stopped: true, sessionId: session.id, watchdogStopped, monitorStopped, runCompleted })}\n`,
569
569
  );
570
570
  } else {
571
- process.stdout.write(`Coordinator stopped (session: ${session.id})\n`);
571
+ printSuccess("Coordinator stopped", session.id);
572
572
  if (watchdogStopped) {
573
- process.stdout.write("Watchdog stopped\n");
573
+ printHint("Watchdog stopped");
574
574
  } else {
575
- process.stdout.write("No watchdog running\n");
575
+ printHint("No watchdog running");
576
576
  }
577
577
  if (monitorStopped) {
578
- process.stdout.write("Monitor stopped\n");
578
+ printHint("Monitor stopped");
579
579
  } else {
580
- process.stdout.write("No monitor running\n");
580
+ printHint("No monitor running");
581
581
  }
582
582
  if (runCompleted) {
583
- process.stdout.write("Run completed\n");
583
+ printHint("Run completed");
584
584
  } else {
585
- process.stdout.write("No active run\n");
585
+ printHint("No active run");
586
586
  }
587
587
  }
588
588
  } finally {
@@ -633,12 +633,12 @@ async function statusCoordinator(
633
633
  `${JSON.stringify({ running: false, watchdogRunning, monitorRunning })}\n`,
634
634
  );
635
635
  } else {
636
- process.stdout.write("Coordinator is not running\n");
636
+ printHint("Coordinator is not running");
637
637
  if (watchdogRunning) {
638
- process.stdout.write("Watchdog: running\n");
638
+ printHint("Watchdog: running");
639
639
  }
640
640
  if (monitorRunning) {
641
- process.stdout.write("Monitor: running\n");
641
+ printHint("Monitor: running");
642
642
  }
643
643
  }
644
644
  return;
@@ -11,6 +11,7 @@ import { join } from "node:path";
11
11
  import { Command } from "commander";
12
12
  import { loadConfig } from "../config.ts";
13
13
  import { GroupError, ValidationError } from "../errors.ts";
14
+ import { printHint, printSuccess } from "../logging/color.ts";
14
15
  import { createTrackerClient, resolveBackend, type TrackerClient } from "../tracker/factory.ts";
15
16
  import type { TaskGroup, TaskGroupProgress } from "../types.ts";
16
17
 
@@ -326,7 +327,7 @@ export function createGroupCommand(): Command {
326
327
  if (opts.json) {
327
328
  process.stdout.write(`${JSON.stringify(group, null, "\t")}\n`);
328
329
  } else {
329
- process.stdout.write(`Created group "${group.name}" (${group.id})\n`);
330
+ printSuccess("Created group", group.name);
330
331
  process.stdout.write(` Members: ${group.memberIssueIds.join(", ")}\n`);
331
332
  }
332
333
  },
@@ -365,7 +366,7 @@ export function createGroupCommand(): Command {
365
366
  if (json) {
366
367
  process.stdout.write("[]\n");
367
368
  } else {
368
- process.stdout.write("No active groups\n");
369
+ printHint("No active groups");
369
370
  }
370
371
  return;
371
372
  }
@@ -414,7 +415,7 @@ export function createGroupCommand(): Command {
414
415
  if (opts.json) {
415
416
  process.stdout.write(`${JSON.stringify(group, null, "\t")}\n`);
416
417
  } else {
417
- process.stdout.write(`Added ${ids.length} issue(s) to "${group.name}"\n`);
418
+ printSuccess("Added to group", group.name);
418
419
  process.stdout.write(` Members: ${group.memberIssueIds.join(", ")}\n`);
419
420
  }
420
421
  },
@@ -434,7 +435,7 @@ export function createGroupCommand(): Command {
434
435
  if (opts.json) {
435
436
  process.stdout.write(`${JSON.stringify(group, null, "\t")}\n`);
436
437
  } else {
437
- process.stdout.write(`Removed ${ids.length} issue(s) from "${group.name}"\n`);
438
+ printSuccess("Removed from group", group.name);
438
439
  process.stdout.write(` Members: ${group.memberIssueIds.join(", ")}\n`);
439
440
  }
440
441
  });
@@ -452,7 +453,7 @@ export function createGroupCommand(): Command {
452
453
  if (opts.json) {
453
454
  process.stdout.write("[]\n");
454
455
  } else {
455
- process.stdout.write("No groups\n");
456
+ printHint("No groups");
456
457
  }
457
458
  return;
458
459
  }
@@ -227,7 +227,7 @@ describe("hooks uninstall", () => {
227
227
  );
228
228
 
229
229
  const output = await captureStdout(() => hooksCommand(["uninstall"]));
230
- expect(output).toContain("preserved other settings");
230
+ expect(output).toContain("Removed");
231
231
 
232
232
  const content = await Bun.file(join(claudeDir, "settings.local.json")).text();
233
233
  const parsed = JSON.parse(content) as Record<string, unknown>;
@@ -15,6 +15,7 @@ import { join } from "node:path";
15
15
  import { Command } from "commander";
16
16
  import { loadConfig } from "../config.ts";
17
17
  import { ValidationError } from "../errors.ts";
18
+ import { printHint, printSuccess, printWarning } from "../logging/color.ts";
18
19
 
19
20
  interface HookEntry {
20
21
  matcher: string;
@@ -120,8 +121,8 @@ async function installHooks(force: boolean): Promise<void> {
120
121
  await mkdir(targetDir, { recursive: true });
121
122
  await Bun.write(targetPath, `${JSON.stringify(targetConfig, null, "\t")}\n`);
122
123
 
123
- process.stdout.write("\u2713 Installed orchestrator hooks to .claude/settings.local.json\n");
124
- process.stdout.write(" Source: .overstory/hooks.json\n");
124
+ printSuccess("Installed orchestrator hooks");
125
+ printHint("Source: .overstory/hooks.json");
125
126
  }
126
127
 
127
128
  /**
@@ -139,7 +140,7 @@ async function uninstallHooks(): Promise<void> {
139
140
  const targetFile = Bun.file(targetPath);
140
141
 
141
142
  if (!(await targetFile.exists())) {
142
- process.stdout.write("No .claude/settings.local.json found \u2014 nothing to uninstall.\n");
143
+ printWarning("No .claude/settings.local.json found", "nothing to uninstall");
143
144
  return;
144
145
  }
145
146
 
@@ -159,12 +160,10 @@ async function uninstallHooks(): Promise<void> {
159
160
  const remainingKeys = Object.keys(rest);
160
161
  if (remainingKeys.length === 0) {
161
162
  await unlink(targetPath);
162
- process.stdout.write("\u2713 Removed .claude/settings.local.json (was hooks-only)\n");
163
+ printSuccess("Removed hooks from settings.local.json");
163
164
  } else {
164
165
  await Bun.write(targetPath, `${JSON.stringify(rest, null, "\t")}\n`);
165
- process.stdout.write(
166
- "\u2713 Removed hooks from .claude/settings.local.json (preserved other settings)\n",
167
- );
166
+ printSuccess("Removed hooks from settings.local.json");
168
167
  }
169
168
  }
170
169
 
@@ -199,7 +198,7 @@ async function statusHooks(json: boolean): Promise<void> {
199
198
  `Hooks installed (.claude/settings.local.json): ${installed ? "yes" : "no"}\n`,
200
199
  );
201
200
  if (!installed && sourceExists) {
202
- process.stdout.write(`\nRun 'ov hooks install' to install.\n`);
201
+ printHint("Run ov hooks install to install");
203
202
  }
204
203
  }
205
204
  }
@@ -20,10 +20,6 @@ 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",
27
23
  ];
28
24
 
29
25
  /** Resolve the source agents directory (same logic as init.ts). */
@@ -50,7 +46,7 @@ describe("initCommand: agent-defs deployment", () => {
50
46
  await cleanupTempDir(tempDir);
51
47
  });
52
48
 
53
- test("creates .overstory/agent-defs/ with all 12 agent definition files", async () => {
49
+ test("creates .overstory/agent-defs/ with all 8 agent definition files", async () => {
54
50
  await initCommand({});
55
51
 
56
52
  const agentDefsDir = join(tempDir, ".overstory", "agent-defs");
@@ -14,6 +14,7 @@ import { mkdir, readdir } from "node:fs/promises";
14
14
  import { basename, join } from "node:path";
15
15
  import { DEFAULT_CONFIG } from "../config.ts";
16
16
  import { ValidationError } from "../errors.ts";
17
+ import { printHint, printSuccess } from "../logging/color.ts";
17
18
  import type { AgentManifest, OverstoryConfig } from "../types.ts";
18
19
 
19
20
  const OVERSTORY_DIR = ".overstory";
@@ -522,7 +523,7 @@ export interface InitOptions {
522
523
  * Print a success status line.
523
524
  */
524
525
  function printCreated(relativePath: string): void {
525
- process.stdout.write(` \u2713 Created ${relativePath}\n`);
526
+ printSuccess("Created", relativePath);
526
527
  }
527
528
 
528
529
  /**
@@ -631,11 +632,11 @@ export async function initCommand(opts: InitOptions): Promise<void> {
631
632
  if (force) {
632
633
  const migrated = await migrateExistingDatabases(overstoryPath);
633
634
  for (const dbName of migrated) {
634
- process.stdout.write(` \u2713 Migrated ${OVERSTORY_DIR}/${dbName} (schema validated)\n`);
635
+ printSuccess("Migrated", dbName);
635
636
  }
636
637
  }
637
638
 
638
- process.stdout.write("\nDone.\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");
639
+ printSuccess("Initialized");
640
+ printHint("Next: run `ov hooks install` to enable Claude Code hooks.");
641
+ printHint("Then: run `ov status` to see the current state.");
641
642
  }
@@ -100,7 +100,7 @@ describe("mailCommand", () => {
100
100
  client.close();
101
101
 
102
102
  await mailCommand(["list", "--agent", "builder-1", "--unread"]);
103
- expect(output).toContain("No messages found.");
103
+ expect(output).toContain("No messages found");
104
104
  });
105
105
 
106
106
  test("--to takes precedence over --agent when both provided", async () => {
@@ -134,7 +134,7 @@ describe("mailCommand", () => {
134
134
  output = "";
135
135
  await mailCommand(["reply", originalId, "--body", "Actually also do Y"]);
136
136
 
137
- expect(output).toContain("Reply sent:");
137
+ expect(output).toContain("Reply sent");
138
138
 
139
139
  // Verify the reply went to builder-1, not back to orchestrator
140
140
  const store2 = createMailStore(join(tempDir, ".overstory", "mail.db"));
@@ -162,7 +162,7 @@ describe("mailCommand", () => {
162
162
  output = "";
163
163
  await mailCommand(["reply", originalId, "--body", "Done", "--agent", "builder-1"]);
164
164
 
165
- expect(output).toContain("Reply sent:");
165
+ expect(output).toContain("Reply sent");
166
166
 
167
167
  // Verify the reply went to orchestrator (original sender)
168
168
  const store2 = createMailStore(join(tempDir, ".overstory", "mail.db"));
@@ -193,7 +193,7 @@ describe("mailCommand", () => {
193
193
  output = "";
194
194
  await mailCommand(["reply", "--agent", "scout-1", "--body", "Got it", originalId]);
195
195
 
196
- expect(output).toContain("Reply sent:");
196
+ expect(output).toContain("Reply sent");
197
197
 
198
198
  // Verify the reply used the correct message ID (not 'scout-1' or 'Got it')
199
199
  const store2 = createMailStore(join(tempDir, ".overstory", "mail.db"));
@@ -223,7 +223,7 @@ describe("mailCommand", () => {
223
223
  output = "";
224
224
  await mailCommand(["read", originalId]);
225
225
 
226
- expect(output).toContain(`Marked ${originalId} as read.`);
226
+ expect(output).toContain("Marked as read");
227
227
  });
228
228
 
229
229
  test("read marks message as read", async () => {
@@ -238,7 +238,7 @@ describe("mailCommand", () => {
238
238
 
239
239
  output = "";
240
240
  await mailCommand(["read", originalId]);
241
- expect(output).toContain(`Marked ${originalId} as read.`);
241
+ expect(output).toContain("Marked as read");
242
242
 
243
243
  // Reading again should show already read
244
244
  output = "";