@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.
- package/README.md +5 -2
- package/agents/coordinator.md +5 -5
- package/agents/lead.md +1 -6
- package/agents/merger.md +3 -3
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +3 -3
- package/agents/supervisor.md +16 -16
- package/package.json +1 -1
- package/src/agents/hooks-deployer.test.ts +180 -0
- package/src/agents/hooks-deployer.ts +32 -1
- package/src/commands/agents.ts +9 -6
- package/src/commands/clean.ts +5 -3
- package/src/commands/completions.ts +3 -4
- package/src/commands/coordinator.test.ts +15 -12
- package/src/commands/coordinator.ts +28 -25
- package/src/commands/costs.test.ts +48 -38
- package/src/commands/costs.ts +48 -38
- package/src/commands/dashboard.ts +7 -7
- package/src/commands/doctor.test.ts +8 -0
- package/src/commands/doctor.ts +2 -6
- package/src/commands/errors.test.ts +47 -40
- package/src/commands/errors.ts +5 -4
- package/src/commands/feed.test.ts +40 -33
- package/src/commands/feed.ts +3 -2
- package/src/commands/group.ts +28 -18
- package/src/commands/hooks.test.ts +1 -1
- package/src/commands/hooks.ts +9 -9
- package/src/commands/init.test.ts +105 -5
- package/src/commands/init.ts +17 -12
- package/src/commands/inspect.test.ts +2 -0
- package/src/commands/inspect.ts +9 -8
- package/src/commands/logs.test.ts +5 -6
- package/src/commands/logs.ts +2 -1
- package/src/commands/mail.test.ts +17 -16
- package/src/commands/mail.ts +17 -17
- package/src/commands/merge.ts +12 -12
- package/src/commands/metrics.test.ts +15 -2
- package/src/commands/metrics.ts +3 -2
- package/src/commands/monitor.ts +9 -7
- package/src/commands/nudge.ts +4 -4
- package/src/commands/prime.test.ts +1 -6
- package/src/commands/prime.ts +2 -3
- package/src/commands/replay.test.ts +62 -55
- package/src/commands/replay.ts +3 -2
- package/src/commands/run.ts +24 -26
- package/src/commands/sling.ts +4 -2
- package/src/commands/spec.test.ts +10 -8
- package/src/commands/spec.ts +3 -2
- package/src/commands/status.test.ts +2 -1
- package/src/commands/status.ts +7 -6
- package/src/commands/stop.test.ts +10 -6
- package/src/commands/stop.ts +13 -13
- package/src/commands/supervisor.ts +12 -10
- package/src/commands/trace.test.ts +52 -44
- package/src/commands/trace.ts +5 -4
- package/src/commands/watch.ts +8 -10
- package/src/commands/worktree.test.ts +27 -20
- package/src/commands/worktree.ts +29 -30
- package/src/doctor/version.ts +2 -2
- package/src/e2e/init-sling-lifecycle.test.ts +1 -5
- package/src/index.ts +99 -14
- package/src/json.test.ts +72 -0
- package/src/json.ts +24 -0
- package/src/logging/color.test.ts +127 -0
- package/src/logging/color.ts +28 -0
- package/src/mulch/client.test.ts +22 -22
- package/src/worktree/tmux.test.ts +123 -5
- package/src/worktree/tmux.ts +38 -8
- package/agents/issue-reviews.md +0 -71
- package/agents/pr-reviews.md +0 -60
- package/agents/prioritize.md +0 -110
- 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` (
|
|
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 (
|
|
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
|
package/agents/coordinator.md
CHANGED
|
@@ -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
|
|
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,
|
|
137
|
-
- `merged` -- merger confirms successful merge (branch,
|
|
138
|
-
- `merge_failed` -- merger reports merge failure (branch,
|
|
139
|
-
- `escalation` -- any agent escalates an issue (severity: warning|error|critical,
|
|
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
|
|
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 `
|
|
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
|
|
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-
|
|
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
|
package/agents/reviewer.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 (task ID, code to review, branch name, parent agent) is in `.claude/CLAUDE.md` in your worktree. That file is generated by `
|
|
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
|
|
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 `
|
|
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
|
|
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-
|
|
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
|
package/agents/supervisor.md
CHANGED
|
@@ -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 (
|
|
137
|
-
- `merge_ready` -- signal to coordinator that a branch is verified and ready for merge (branch,
|
|
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,
|
|
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 (
|
|
145
|
-
- `worker_done` -- worker signals completion (
|
|
146
|
-
- `merged` -- merger confirms successful merge (branch,
|
|
147
|
-
- `merge_failed` -- merger reports merge failure (branch,
|
|
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 (
|
|
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 <
|
|
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>", "
|
|
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,
|
|
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 <
|
|
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,
|
|
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", "
|
|
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", "
|
|
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", "
|
|
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.
|
|
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
|
|
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);
|
package/src/commands/agents.ts
CHANGED
|
@@ -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 {
|
|
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(
|
|
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
|
-
|
|
226
|
+
jsonOutput("agents discover", { agents });
|
|
224
227
|
} else {
|
|
225
228
|
printAgents(agents);
|
|
226
229
|
}
|
package/src/commands/clean.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|