@os-eco/overstory-cli 0.6.7 → 0.6.9

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 (72) hide show
  1. package/README.md +5 -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/agents/hooks-deployer.test.ts +180 -0
  10. package/src/agents/hooks-deployer.ts +32 -1
  11. package/src/commands/agents.ts +9 -6
  12. package/src/commands/clean.ts +5 -3
  13. package/src/commands/completions.ts +3 -4
  14. package/src/commands/coordinator.test.ts +15 -12
  15. package/src/commands/coordinator.ts +28 -25
  16. package/src/commands/costs.test.ts +48 -38
  17. package/src/commands/costs.ts +48 -38
  18. package/src/commands/dashboard.ts +7 -7
  19. package/src/commands/doctor.test.ts +8 -0
  20. package/src/commands/doctor.ts +2 -6
  21. package/src/commands/errors.test.ts +47 -40
  22. package/src/commands/errors.ts +5 -4
  23. package/src/commands/feed.test.ts +40 -33
  24. package/src/commands/feed.ts +3 -2
  25. package/src/commands/group.ts +28 -18
  26. package/src/commands/hooks.test.ts +1 -1
  27. package/src/commands/hooks.ts +9 -9
  28. package/src/commands/init.test.ts +105 -5
  29. package/src/commands/init.ts +17 -12
  30. package/src/commands/inspect.test.ts +2 -0
  31. package/src/commands/inspect.ts +9 -8
  32. package/src/commands/logs.test.ts +5 -6
  33. package/src/commands/logs.ts +2 -1
  34. package/src/commands/mail.test.ts +17 -16
  35. package/src/commands/mail.ts +17 -17
  36. package/src/commands/merge.ts +12 -12
  37. package/src/commands/metrics.test.ts +15 -2
  38. package/src/commands/metrics.ts +3 -2
  39. package/src/commands/monitor.ts +9 -7
  40. package/src/commands/nudge.ts +4 -4
  41. package/src/commands/prime.test.ts +1 -6
  42. package/src/commands/prime.ts +2 -3
  43. package/src/commands/replay.test.ts +62 -55
  44. package/src/commands/replay.ts +3 -2
  45. package/src/commands/run.ts +24 -26
  46. package/src/commands/sling.ts +4 -2
  47. package/src/commands/spec.test.ts +10 -8
  48. package/src/commands/spec.ts +3 -2
  49. package/src/commands/status.test.ts +2 -1
  50. package/src/commands/status.ts +7 -6
  51. package/src/commands/stop.test.ts +10 -6
  52. package/src/commands/stop.ts +13 -13
  53. package/src/commands/supervisor.ts +12 -10
  54. package/src/commands/trace.test.ts +52 -44
  55. package/src/commands/trace.ts +5 -4
  56. package/src/commands/watch.ts +8 -10
  57. package/src/commands/worktree.test.ts +27 -20
  58. package/src/commands/worktree.ts +29 -30
  59. package/src/doctor/version.ts +2 -2
  60. package/src/e2e/init-sling-lifecycle.test.ts +1 -5
  61. package/src/index.ts +99 -14
  62. package/src/json.test.ts +72 -0
  63. package/src/json.ts +24 -0
  64. package/src/logging/color.test.ts +127 -0
  65. package/src/logging/color.ts +28 -0
  66. package/src/mulch/client.test.ts +22 -22
  67. package/src/worktree/tmux.test.ts +123 -5
  68. package/src/worktree/tmux.ts +38 -8
  69. package/agents/issue-reviews.md +0 -71
  70. package/agents/pr-reviews.md +0 -60
  71. package/agents/prioritize.md +0 -110
  72. package/agents/release.md +0 -56
package/README.md CHANGED
@@ -105,6 +105,8 @@ ov agents discover Discover agents by capability/state/parent
105
105
 
106
106
  ov init Initialize .overstory/ in current project
107
107
  (deploys agent definitions automatically)
108
+ --yes, -y Skip interactive prompts
109
+ --name <name> Set project name (default: auto-detect)
108
110
 
109
111
  ov coordinator start Start persistent coordinator agent
110
112
  --attach / --no-attach TTY-aware tmux attach (default: auto)
@@ -273,13 +275,13 @@ Global Flags:
273
275
  - **Dependencies**: Minimal runtime — `chalk` (color output), `commander` (CLI framework), core I/O via Bun built-in APIs
274
276
  - **Database**: SQLite via `bun:sqlite` (WAL mode for concurrent access)
275
277
  - **Linting**: Biome (formatter + linter)
276
- - **Testing**: `bun test` (2151 tests across 76 files, colocated with source)
278
+ - **Testing**: `bun test` (2186 tests across 77 files, colocated with source)
277
279
  - **External CLIs**: `bd` (beads) or `sd` (seeds), `mulch`, `git`, `tmux` — invoked as subprocesses
278
280
 
279
281
  ## Development
280
282
 
281
283
  ```bash
282
- # Run tests (2151 tests across 76 files)
284
+ # Run tests (2186 tests across 77 files)
283
285
  bun test
284
286
 
285
287
  # Run a single test
@@ -319,6 +321,7 @@ overstory/
319
321
  types.ts Shared types and interfaces
320
322
  config.ts Config loader + validation
321
323
  errors.ts Custom error types
324
+ json.ts Standardized JSON envelope helpers
322
325
  commands/ One file per CLI subcommand (30 commands)
323
326
  agents.ts Agent discovery and querying
324
327
  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.9",
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",
@@ -14,6 +14,7 @@ import {
14
14
  getDangerGuards,
15
15
  getPathBoundaryGuards,
16
16
  isOverstoryHookEntry,
17
+ PATH_PREFIX,
17
18
  } from "./hooks-deployer.ts";
18
19
 
19
20
  describe("deployHooks", () => {
@@ -2115,6 +2116,185 @@ describe("bash path boundary integration", () => {
2115
2116
  });
2116
2117
  });
2117
2118
 
2119
+ describe("PATH_PREFIX", () => {
2120
+ test("PATH_PREFIX is exported and is a non-empty string", () => {
2121
+ expect(typeof PATH_PREFIX).toBe("string");
2122
+ expect(PATH_PREFIX.length).toBeGreaterThan(0);
2123
+ });
2124
+
2125
+ test("PATH_PREFIX contains ~/.bun/bin for bun-installed CLIs", () => {
2126
+ expect(PATH_PREFIX).toContain(".bun/bin");
2127
+ });
2128
+
2129
+ test("PATH_PREFIX extends PATH (not replaces it)", () => {
2130
+ // Must preserve original PATH via :$PATH
2131
+ expect(PATH_PREFIX).toContain(":$PATH");
2132
+ });
2133
+
2134
+ test("PATH_PREFIX sets PATH via export", () => {
2135
+ expect(PATH_PREFIX).toMatch(/^export PATH=/);
2136
+ });
2137
+ });
2138
+
2139
+ describe("PATH prefix in deployed hooks", () => {
2140
+ let tempDir: string;
2141
+
2142
+ beforeEach(async () => {
2143
+ tempDir = await mkdtemp(join(tmpdir(), "overstory-path-prefix-test-"));
2144
+ });
2145
+
2146
+ afterEach(async () => {
2147
+ await rm(tempDir, { recursive: true, force: true });
2148
+ });
2149
+
2150
+ test("SessionStart hook commands include PATH prefix", async () => {
2151
+ const worktreePath = join(tempDir, "path-ss-wt");
2152
+ await deployHooks(worktreePath, "path-agent");
2153
+
2154
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2155
+ const parsed = JSON.parse(content);
2156
+ for (const entry of parsed.hooks.SessionStart) {
2157
+ for (const hook of entry.hooks) {
2158
+ expect(hook.command).toContain("export PATH=");
2159
+ expect(hook.command).toContain(".bun/bin");
2160
+ }
2161
+ }
2162
+ });
2163
+
2164
+ test("UserPromptSubmit hook commands include PATH prefix", async () => {
2165
+ const worktreePath = join(tempDir, "path-ups-wt");
2166
+ await deployHooks(worktreePath, "path-agent");
2167
+
2168
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2169
+ const parsed = JSON.parse(content);
2170
+ for (const entry of parsed.hooks.UserPromptSubmit) {
2171
+ for (const hook of entry.hooks) {
2172
+ expect(hook.command).toContain("export PATH=");
2173
+ }
2174
+ }
2175
+ });
2176
+
2177
+ test("PostToolUse hook commands include PATH prefix", async () => {
2178
+ const worktreePath = join(tempDir, "path-ptu-wt");
2179
+ await deployHooks(worktreePath, "path-agent");
2180
+
2181
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2182
+ const parsed = JSON.parse(content);
2183
+ for (const entry of parsed.hooks.PostToolUse) {
2184
+ for (const hook of entry.hooks) {
2185
+ expect(hook.command).toContain("export PATH=");
2186
+ }
2187
+ }
2188
+ });
2189
+
2190
+ test("Stop hook commands include PATH prefix", async () => {
2191
+ const worktreePath = join(tempDir, "path-stop-wt");
2192
+ await deployHooks(worktreePath, "path-agent");
2193
+
2194
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2195
+ const parsed = JSON.parse(content);
2196
+ for (const entry of parsed.hooks.Stop) {
2197
+ for (const hook of entry.hooks) {
2198
+ expect(hook.command).toContain("export PATH=");
2199
+ expect(hook.command).toContain(".bun/bin");
2200
+ }
2201
+ }
2202
+ });
2203
+
2204
+ test("PreCompact hook commands include PATH prefix", async () => {
2205
+ const worktreePath = join(tempDir, "path-pc-wt");
2206
+ await deployHooks(worktreePath, "path-agent");
2207
+
2208
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2209
+ const parsed = JSON.parse(content);
2210
+ for (const entry of parsed.hooks.PreCompact) {
2211
+ for (const hook of entry.hooks) {
2212
+ expect(hook.command).toContain("export PATH=");
2213
+ }
2214
+ }
2215
+ });
2216
+
2217
+ test("PATH prefix appears before CLI command in SessionStart", async () => {
2218
+ const worktreePath = join(tempDir, "path-order-wt");
2219
+ await deployHooks(worktreePath, "path-order-agent");
2220
+
2221
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2222
+ const parsed = JSON.parse(content);
2223
+ const cmd = parsed.hooks.SessionStart[0].hooks[0].command as string;
2224
+ // PATH export must come before the CLI invocation
2225
+ const pathIdx = cmd.indexOf("export PATH=");
2226
+ const ovIdx = cmd.indexOf("ov prime");
2227
+ expect(pathIdx).toBeGreaterThanOrEqual(0);
2228
+ expect(ovIdx).toBeGreaterThan(pathIdx);
2229
+ });
2230
+
2231
+ test("PATH prefix appears before ml learn in Stop hook", async () => {
2232
+ const worktreePath = join(tempDir, "path-ml-wt");
2233
+ await deployHooks(worktreePath, "path-ml-agent");
2234
+
2235
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2236
+ const parsed = JSON.parse(content);
2237
+ const stopHooks = parsed.hooks.Stop[0].hooks;
2238
+ // Second Stop hook is "ml learn"
2239
+ const mlCmd = stopHooks[1].command as string;
2240
+ const pathIdx = mlCmd.indexOf("export PATH=");
2241
+ const mlIdx = mlCmd.indexOf("ml learn");
2242
+ expect(pathIdx).toBeGreaterThanOrEqual(0);
2243
+ expect(mlIdx).toBeGreaterThan(pathIdx);
2244
+ });
2245
+
2246
+ test("generated guard commands do NOT have PATH prefix (they use only built-ins)", async () => {
2247
+ const worktreePath = join(tempDir, "path-guards-wt");
2248
+ await deployHooks(worktreePath, "path-guards-agent", "builder");
2249
+
2250
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2251
+ const parsed = JSON.parse(content);
2252
+ const preToolUse = parsed.hooks.PreToolUse;
2253
+
2254
+ // Path boundary guards (Write/Edit/NotebookEdit) are generated — no PATH prefix
2255
+ const writeGuard = preToolUse.find(
2256
+ (h: { matcher: string; hooks: Array<{ command: string }> }) =>
2257
+ h.matcher === "Write" && h.hooks[0]?.command?.includes("OVERSTORY_WORKTREE_PATH"),
2258
+ );
2259
+ expect(writeGuard).toBeDefined();
2260
+ expect(writeGuard.hooks[0].command).not.toContain("export PATH=");
2261
+
2262
+ // Danger guard (generated) — no PATH prefix
2263
+ const dangerGuard = preToolUse.find(
2264
+ (h: { matcher: string; hooks: Array<{ command: string }> }) =>
2265
+ h.matcher === "Bash" && h.hooks[0]?.command?.includes("git reset --hard"),
2266
+ );
2267
+ expect(dangerGuard).toBeDefined();
2268
+ expect(dangerGuard.hooks[0].command).not.toContain("export PATH=");
2269
+ });
2270
+
2271
+ test("re-deployment is idempotent: PATH prefix not duplicated", async () => {
2272
+ const worktreePath = join(tempDir, "path-idem-wt");
2273
+
2274
+ await deployHooks(worktreePath, "path-idem-agent");
2275
+ await deployHooks(worktreePath, "path-idem-agent");
2276
+
2277
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2278
+ const parsed = JSON.parse(content);
2279
+ const cmd = parsed.hooks.SessionStart[0].hooks[0].command as string;
2280
+
2281
+ // PATH prefix should appear exactly once, not doubled
2282
+ const occurrences = cmd.split("export PATH=").length - 1;
2283
+ expect(occurrences).toBe(1);
2284
+ });
2285
+
2286
+ test("PATH prefix uses $HOME expansion (not hardcoded path)", async () => {
2287
+ const worktreePath = join(tempDir, "path-home-wt");
2288
+ await deployHooks(worktreePath, "home-agent");
2289
+
2290
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2291
+ const parsed = JSON.parse(content);
2292
+ const cmd = parsed.hooks.SessionStart[0].hooks[0].command as string;
2293
+ // Should use $HOME not a hardcoded path like /Users/...
2294
+ expect(cmd).toContain("$HOME");
2295
+ });
2296
+ });
2297
+
2118
2298
  describe("escapeForSingleQuotedShell", () => {
2119
2299
  test("no single quotes: string passes through unchanged", () => {
2120
2300
  expect(escapeForSingleQuotedShell("hello world")).toBe("hello world");
@@ -149,6 +149,22 @@ function getTemplatePath(): string {
149
149
  */
150
150
  const ENV_GUARD = '[ -z "$OVERSTORY_AGENT_NAME" ] && exit 0;';
151
151
 
152
+ /**
153
+ * PATH setup prefix for hook commands.
154
+ *
155
+ * Claude Code executes hook commands via /bin/sh with a minimal PATH
156
+ * (/usr/bin:/bin:/usr/sbin:/sbin). Bun-installed CLIs — ov, ml, sd, cn, bd —
157
+ * live in ~/.bun/bin which is absent from that PATH, causing hooks like
158
+ * `ov prime` (SessionStart) and `ml learn` (Stop) to fail with
159
+ * "command not found".
160
+ *
161
+ * Prepend this to any hook command that invokes one of those CLIs so they
162
+ * resolve correctly regardless of how Claude Code was launched.
163
+ *
164
+ * Exported so tests can verify the exact prefix value.
165
+ */
166
+ export const PATH_PREFIX = 'export PATH="$HOME/.bun/bin:/usr/local/bin:/opt/homebrew/bin:$PATH";';
167
+
152
168
  /**
153
169
  * Build a PreToolUse guard script that validates file paths are within
154
170
  * the agent's worktree boundary.
@@ -571,8 +587,23 @@ export async function deployHooks(
571
587
  content = content.replace("{{AGENT_NAME}}", agentName);
572
588
  }
573
589
 
574
- // Parse the base config and merge guards into PreToolUse
590
+ // Parse the base config from the template
575
591
  const config = JSON.parse(content) as { hooks: Record<string, HookEntry[]> };
592
+
593
+ // Extend PATH in all template hook commands.
594
+ // Claude Code invokes hooks with PATH=/usr/bin:/bin:/usr/sbin:/sbin — ~/.bun/bin
595
+ // (where ov, ml, sd, etc. live) is not included. Prepend PATH_PREFIX so CLIs resolve.
596
+ for (const entries of Object.values(config.hooks)) {
597
+ for (const entry of entries) {
598
+ for (const hook of entry.hooks) {
599
+ hook.command = `${PATH_PREFIX} ${hook.command}`;
600
+ }
601
+ }
602
+ }
603
+
604
+ // Merge capability-specific PreToolUse guards into the config.
605
+ // Guards are generated scripts using only shell built-ins (grep, sed, echo, exit)
606
+ // and do not require PATH extension.
576
607
  const pathGuards = getPathBoundaryGuards();
577
608
  const dangerGuards = getDangerGuards(agentName);
578
609
  const capabilityGuards = getCapabilityGuards(capability);
@@ -8,7 +8,8 @@ import { join } from "node:path";
8
8
  import { Command } from "commander";
9
9
  import { loadConfig } from "../config.ts";
10
10
  import { ValidationError } from "../errors.ts";
11
- import { color } from "../logging/color.ts";
11
+ import { jsonOutput } from "../json.ts";
12
+ import { accent, color } from "../logging/color.ts";
12
13
  import { openSessionStore } from "../sessions/compat.ts";
13
14
  import { type AgentSession, SUPPORTED_CAPABILITIES } from "../types.ts";
14
15
 
@@ -166,10 +167,12 @@ function printAgents(agents: DiscoveredAgent[]): void {
166
167
 
167
168
  for (const agent of agents) {
168
169
  const icon = getStateIcon(agent.state);
169
- w(` ${icon} ${agent.agentName} [${agent.capability}]\n`);
170
- w(` State: ${agent.state} | Task: ${agent.taskId}\n`);
171
- w(` Branch: ${agent.branchName}\n`);
172
- w(` Parent: ${agent.parentAgent ?? "none"} | Depth: ${agent.depth}\n`);
170
+ w(` ${icon} ${accent(agent.agentName)} [${agent.capability}]\n`);
171
+ w(` State: ${agent.state} | Task: ${accent(agent.taskId)}\n`);
172
+ w(` Branch: ${accent(agent.branchName)}\n`);
173
+ w(
174
+ ` Parent: ${agent.parentAgent ? accent(agent.parentAgent) : "none"} | Depth: ${agent.depth}\n`,
175
+ );
173
176
 
174
177
  if (agent.fileScope.length === 0) {
175
178
  w(" Files: (unrestricted)\n");
@@ -220,7 +223,7 @@ export function createAgentsCommand(): Command {
220
223
  });
221
224
 
222
225
  if (opts.json) {
223
- process.stdout.write(`${JSON.stringify(agents, null, "\t")}\n`);
226
+ jsonOutput("agents discover", { agents });
224
227
  } else {
225
228
  printAgents(agents);
226
229
  }
@@ -25,6 +25,8 @@ 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 { jsonOutput } from "../json.ts";
29
+ import { printHint, printSuccess } from "../logging/color.ts";
28
30
  import { createMulchClient } from "../mulch/client.ts";
29
31
  import { openSessionStore } from "../sessions/compat.ts";
30
32
  import type { AgentSession, MulchDoctorResult, MulchPruneResult, MulchStatus } from "../types.ts";
@@ -517,7 +519,7 @@ export async function cleanCommand(opts: CleanOptions): Promise<void> {
517
519
 
518
520
  // Output
519
521
  if (json) {
520
- process.stdout.write(`${JSON.stringify(result, null, "\t")}\n`);
522
+ jsonOutput("clean", { ...result });
521
523
  return;
522
524
  }
523
525
 
@@ -584,7 +586,7 @@ export async function cleanCommand(opts: CleanOptions): Promise<void> {
584
586
  }
585
587
 
586
588
  if (lines.length === 0) {
587
- process.stdout.write("Nothing to clean.\n");
589
+ printHint("Nothing to clean");
588
590
  } else {
589
591
  if (result.mulchHealth?.checked) {
590
592
  process.stdout.write("\n--- Cleanup Results ---\n");
@@ -592,6 +594,6 @@ export async function cleanCommand(opts: CleanOptions): Promise<void> {
592
594
  for (const line of lines) {
593
595
  process.stdout.write(`${line}\n`);
594
596
  }
595
- process.stdout.write("\nClean complete.\n");
597
+ printSuccess("Clean complete");
596
598
  }
597
599
  }
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { Command } from "commander";
8
+ import { printError } from "../logging/color.ts";
8
9
 
9
10
  interface FlagDef {
10
11
  name: string;
@@ -872,8 +873,7 @@ export function completionsCommand(args: string[]): void {
872
873
  const shell = args[0];
873
874
 
874
875
  if (!shell) {
875
- process.stderr.write("Error: missing shell argument\n");
876
- process.stderr.write("Usage: ov --completions <bash|zsh|fish>\n");
876
+ printError("missing shell argument", "Usage: ov --completions <bash|zsh|fish>");
877
877
  process.exit(1);
878
878
  }
879
879
 
@@ -889,8 +889,7 @@ export function completionsCommand(args: string[]): void {
889
889
  script = generateFish();
890
890
  break;
891
891
  default:
892
- process.stderr.write(`Error: unknown shell '${shell}'\n`);
893
- process.stderr.write("Supported shells: bash, zsh, fish\n");
892
+ printError(`unknown shell '${shell}'`, "Supported shells: bash, zsh, fish");
894
893
  process.exit(1);
895
894
  }
896
895