@os-eco/overstory-cli 0.6.8 → 0.6.10

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 (69) hide show
  1. package/README.md +19 -5
  2. package/agents/builder.md +6 -15
  3. package/agents/lead.md +4 -6
  4. package/agents/merger.md +5 -13
  5. package/agents/reviewer.md +2 -9
  6. package/package.json +1 -1
  7. package/src/agents/hooks-deployer.test.ts +232 -0
  8. package/src/agents/hooks-deployer.ts +54 -8
  9. package/src/agents/overlay.test.ts +156 -1
  10. package/src/agents/overlay.ts +67 -7
  11. package/src/commands/agents.ts +9 -6
  12. package/src/commands/clean.ts +2 -1
  13. package/src/commands/completions.test.ts +8 -20
  14. package/src/commands/completions.ts +7 -6
  15. package/src/commands/coordinator.test.ts +8 -0
  16. package/src/commands/coordinator.ts +11 -8
  17. package/src/commands/costs.test.ts +48 -38
  18. package/src/commands/costs.ts +48 -38
  19. package/src/commands/dashboard.ts +7 -7
  20. package/src/commands/doctor.test.ts +8 -0
  21. package/src/commands/doctor.ts +96 -51
  22. package/src/commands/ecosystem.ts +291 -0
  23. package/src/commands/errors.test.ts +47 -40
  24. package/src/commands/errors.ts +5 -4
  25. package/src/commands/feed.test.ts +40 -33
  26. package/src/commands/feed.ts +5 -4
  27. package/src/commands/group.ts +23 -14
  28. package/src/commands/hooks.ts +2 -1
  29. package/src/commands/init.test.ts +104 -0
  30. package/src/commands/init.ts +11 -7
  31. package/src/commands/inspect.test.ts +2 -0
  32. package/src/commands/inspect.ts +9 -8
  33. package/src/commands/logs.test.ts +5 -6
  34. package/src/commands/logs.ts +2 -1
  35. package/src/commands/mail.test.ts +11 -10
  36. package/src/commands/mail.ts +11 -12
  37. package/src/commands/merge.ts +11 -12
  38. package/src/commands/metrics.test.ts +15 -2
  39. package/src/commands/metrics.ts +3 -2
  40. package/src/commands/monitor.ts +5 -4
  41. package/src/commands/nudge.ts +2 -3
  42. package/src/commands/prime.test.ts +1 -6
  43. package/src/commands/prime.ts +2 -3
  44. package/src/commands/replay.test.ts +62 -55
  45. package/src/commands/replay.ts +3 -2
  46. package/src/commands/run.ts +17 -20
  47. package/src/commands/sling.ts +3 -2
  48. package/src/commands/status.test.ts +2 -1
  49. package/src/commands/status.ts +7 -6
  50. package/src/commands/stop.test.ts +2 -0
  51. package/src/commands/stop.ts +10 -11
  52. package/src/commands/supervisor.ts +7 -6
  53. package/src/commands/trace.test.ts +52 -44
  54. package/src/commands/trace.ts +5 -4
  55. package/src/commands/upgrade.test.ts +46 -0
  56. package/src/commands/upgrade.ts +259 -0
  57. package/src/commands/watch.ts +8 -10
  58. package/src/commands/worktree.test.ts +21 -15
  59. package/src/commands/worktree.ts +10 -4
  60. package/src/doctor/databases.test.ts +38 -0
  61. package/src/doctor/databases.ts +7 -10
  62. package/src/doctor/ecosystem.test.ts +307 -0
  63. package/src/doctor/ecosystem.ts +155 -0
  64. package/src/doctor/merge-queue.test.ts +98 -0
  65. package/src/doctor/merge-queue.ts +23 -0
  66. package/src/doctor/structure.test.ts +130 -1
  67. package/src/doctor/structure.ts +87 -1
  68. package/src/doctor/types.ts +5 -2
  69. package/src/index.ts +25 -1
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)
@@ -214,6 +216,15 @@ ov clean Clean up worktrees, sessions, artifacts
214
216
  ov doctor Run health checks on overstory setup
215
217
  --json JSON output
216
218
  --category <name> Run a specific check category only
219
+ --fix Auto-fix fixable issues
220
+
221
+ ov ecosystem Show os-eco tool versions and health
222
+ --json JSON output
223
+
224
+ ov upgrade Upgrade overstory to latest npm version
225
+ --check Compare versions without installing
226
+ --all Upgrade all 4 ecosystem tools
227
+ --json JSON output
217
228
 
218
229
  ov inspect <agent> Deep per-agent inspection
219
230
  --json JSON output
@@ -264,6 +275,7 @@ ov metrics Show session metrics
264
275
 
265
276
  Global Flags:
266
277
  --quiet, -q Suppress non-error output
278
+ --timing Print command execution time to stderr
267
279
  --completions <shell> Generate shell completions (bash, zsh, fish)
268
280
  ```
269
281
 
@@ -273,13 +285,13 @@ Global Flags:
273
285
  - **Dependencies**: Minimal runtime — `chalk` (color output), `commander` (CLI framework), core I/O via Bun built-in APIs
274
286
  - **Database**: SQLite via `bun:sqlite` (WAL mode for concurrent access)
275
287
  - **Linting**: Biome (formatter + linter)
276
- - **Testing**: `bun test` (2167 tests across 77 files, colocated with source)
288
+ - **Testing**: `bun test` (2241 tests across 79 files, colocated with source)
277
289
  - **External CLIs**: `bd` (beads) or `sd` (seeds), `mulch`, `git`, `tmux` — invoked as subprocesses
278
290
 
279
291
  ## Development
280
292
 
281
293
  ```bash
282
- # Run tests (2167 tests across 77 files)
294
+ # Run tests (2241 tests across 79 files)
283
295
  bun test
284
296
 
285
297
  # Run a single test
@@ -320,7 +332,7 @@ overstory/
320
332
  config.ts Config loader + validation
321
333
  errors.ts Custom error types
322
334
  json.ts Standardized JSON envelope helpers
323
- commands/ One file per CLI subcommand (30 commands)
335
+ commands/ One file per CLI subcommand (32 commands)
324
336
  agents.ts Agent discovery and querying
325
337
  coordinator.ts Persistent orchestrator lifecycle
326
338
  supervisor.ts Team lead management
@@ -343,7 +355,7 @@ overstory/
343
355
  run.ts Orchestration run lifecycle
344
356
  trace.ts Agent/bead timeline viewing
345
357
  clean.ts Worktree/session cleanup
346
- doctor.ts Health check runner (9 check modules)
358
+ doctor.ts Health check runner (10 check modules)
347
359
  inspect.ts Deep per-agent inspection
348
360
  spec.ts Task spec management
349
361
  errors.ts Aggregated error view
@@ -351,6 +363,8 @@ overstory/
351
363
  stop.ts Agent termination
352
364
  costs.ts Token/cost analysis
353
365
  metrics.ts Session metrics
366
+ ecosystem.ts os-eco tool dashboard
367
+ upgrade.ts npm version upgrades
354
368
  completions.ts Shell completion generation (bash/zsh/fish)
355
369
  agents/ Agent lifecycle management
356
370
  manifest.ts Agent registry (load + query)
@@ -365,7 +379,7 @@ overstory/
365
379
  watchdog/ Tiered health monitoring (daemon, triage, health)
366
380
  logging/ Multi-format logger + sanitizer + reporter + color control
367
381
  metrics/ SQLite metrics + transcript parsing
368
- doctor/ Health check modules (9 checks)
382
+ doctor/ Health check modules (10 checks)
369
383
  insights/ Session insight analyzer for auto-expertise
370
384
  tracker/ Pluggable task tracker (beads + seeds backends)
371
385
  mulch/ mulch CLI wrapper
package/agents/builder.md CHANGED
@@ -14,8 +14,8 @@ These are named failures. If you catch yourself doing any of these, stop and cor
14
14
  - **FILE_SCOPE_VIOLATION** -- Editing or writing to a file not listed in your FILE_SCOPE. Read any file for context, but only modify scoped files.
15
15
  - **CANONICAL_BRANCH_WRITE** -- Committing to or pushing to main/develop/canonical branch. You commit to your worktree branch only.
16
16
  - **SILENT_FAILURE** -- Encountering an error (test failure, lint failure, blocked dependency) and not reporting it via mail. Every error must be communicated to your parent with `--type error`.
17
- - **INCOMPLETE_CLOSE** -- Running `{{TRACKER_CLI}} close` without first passing quality gates (`bun test`, `bun run lint`, `bun run typecheck`) and sending a result mail to your parent.
18
- - **MISSING_WORKER_DONE** -- Closing a bead issue without first sending `worker_done` mail to parent. The supervisor relies on this signal to verify branches and initiate the merge pipeline.
17
+ - **INCOMPLETE_CLOSE** -- Running `{{TRACKER_CLI}} close` without first passing quality gates ({{QUALITY_GATE_INLINE}}) and sending a result mail to your parent.
18
+ - **MISSING_WORKER_DONE** -- Closing a {{TRACKER_NAME}} issue without first sending `worker_done` mail to parent. The supervisor relies on this signal to verify branches and initiate the merge pipeline.
19
19
  - **MISSING_MULCH_RECORD** -- Closing without recording mulch learnings. Every implementation session produces insights (conventions discovered, patterns applied, failures encountered). Skipping `ml record` loses knowledge for future agents.
20
20
 
21
21
  ## overlay
@@ -29,7 +29,7 @@ Your task-specific context (task ID, file scope, spec path, branch name, parent
29
29
  - **Never push to the canonical branch** (main/develop). You commit to your worktree branch only. Merging is handled by the orchestrator or a merger agent.
30
30
  - **Never run `git push`** -- your branch lives in the local worktree. The merge process handles integration.
31
31
  - **Never spawn sub-workers.** You are a leaf node. If you need something decomposed, ask your parent via mail.
32
- - **Run quality gates before closing.** Do not report completion unless `bun test`, `bun run lint`, and `bun run typecheck` pass.
32
+ - **Run quality gates before closing.** Do not report completion unless {{QUALITY_GATE_INLINE}} pass.
33
33
  - If tests fail, fix them. If you cannot fix them, report the failure via mail with `--type error`.
34
34
 
35
35
  ## communication-protocol
@@ -49,9 +49,7 @@ Your task-specific context (task ID, file scope, spec path, branch name, parent
49
49
 
50
50
  ## completion-protocol
51
51
 
52
- 1. Run `bun test` -- all tests must pass.
53
- 2. Run `bun run lint` -- lint and formatting must be clean.
54
- 3. Run `bun run typecheck` -- no TypeScript errors.
52
+ {{QUALITY_GATE_STEPS}}
55
53
  4. Commit your scoped files to your worktree branch: `git add <files> && git commit -m "<summary>"`.
56
54
  5. **Record mulch learnings** -- review your work for insights worth preserving (conventions discovered, patterns applied, failures encountered, decisions made) and record them with outcome data:
57
55
  ```bash
@@ -88,10 +86,7 @@ You are an implementation specialist. Given a spec and a set of files you own, y
88
86
  - **Grep** -- search file contents with regex
89
87
  - **Bash:**
90
88
  - `git add`, `git commit`, `git diff`, `git log`, `git status`
91
- - `bun test` (run tests)
92
- - `bun run lint` (lint and format check via biome)
93
- - `bun run biome check --write` (auto-fix lint/format issues)
94
- - `bun run typecheck` (type checking via tsc)
89
+ {{QUALITY_GATE_CAPABILITIES}}
95
90
  - `{{TRACKER_CLI}} show`, `{{TRACKER_CLI}} close` ({{TRACKER_NAME}} task management)
96
91
  - `ml prime`, `ml record`, `ml query` (expertise)
97
92
  - `ov mail send`, `ov mail check` (communication)
@@ -116,11 +111,7 @@ You are an implementation specialist. Given a spec and a set of files you own, y
116
111
  - Follow project conventions (check existing code for patterns).
117
112
  - Write tests alongside implementation.
118
113
  5. **Run quality gates:**
119
- ```bash
120
- bun test # All tests must pass
121
- bun run lint # Lint and format must be clean
122
- bun run typecheck # No TypeScript errors
123
- ```
114
+ {{QUALITY_GATE_BASH}}
124
115
  6. **Commit your work** to your worktree branch:
125
116
  ```bash
126
117
  git add <your-scoped-files>
package/agents/lead.md CHANGED
@@ -74,9 +74,7 @@ You are primarily a coordinator, but you can also be a doer for simple tasks. Yo
74
74
  - **Grep** -- search file contents with regex
75
75
  - **Bash:**
76
76
  - `git add`, `git commit`, `git diff`, `git log`, `git status`
77
- - `bun test` (run tests)
78
- - `bun run lint` (lint check)
79
- - `bun run typecheck` (type checking)
77
+ {{QUALITY_GATE_CAPABILITIES}}
80
78
  - `{{TRACKER_CLI}} create`, `{{TRACKER_CLI}} show`, `{{TRACKER_CLI}} ready`, `{{TRACKER_CLI}} close`, `{{TRACKER_CLI}} update` (full {{TRACKER_NAME}} management)
81
79
  - `{{TRACKER_CLI}} sync` (sync {{TRACKER_NAME}} with git)
82
80
  - `ml prime`, `ml record`, `ml query`, `ml search` (expertise)
@@ -230,7 +228,7 @@ Review is a quality investment. For complex, multi-file changes, spawn a reviewe
230
228
  **Self-verification (simple/moderate tasks):**
231
229
  1. Read the builder's diff: `git diff main..<builder-branch>`
232
230
  2. Check the diff matches the spec
233
- 3. Run quality gates: `bun test`, `bun run lint`, `bun run typecheck`
231
+ 3. Run quality gates: {{QUALITY_GATE_INLINE}}
234
232
  4. If everything passes, send merge_ready directly
235
233
 
236
234
  **Reviewer verification (complex tasks):**
@@ -250,7 +248,7 @@ Review is a quality investment. For complex, multi-file changes, spawn a reviewe
250
248
  --body "Review the changes on branch <builder-branch>. Spec: .overstory/specs/<builder-bead-id>.md. Run quality gates and report PASS or FAIL." \
251
249
  --type dispatch
252
250
  ```
253
- The reviewer validates against the builder's spec and runs quality gates (`bun test`, `bun run lint`, `bun run typecheck`).
251
+ The reviewer validates against the builder's spec and runs the project's quality gates ({{QUALITY_GATE_INLINE}}).
254
252
  13. **Handle review results:**
255
253
  - **PASS:** Either the reviewer sends a `result` mail with "PASS" in the subject, or self-verification confirms the diff matches the spec and quality gates pass. Immediately signal `merge_ready` for that builder's branch -- do not wait for other builders to finish:
256
254
  ```bash
@@ -286,7 +284,7 @@ Good decomposition follows these principles:
286
284
 
287
285
  1. **Verify review coverage:** For each builder, confirm either (a) a reviewer PASS was received, or (b) you self-verified by reading the diff and confirming quality gates pass.
288
286
  2. Verify all subtask {{TRACKER_NAME}} issues are closed AND each builder's `merge_ready` has been sent (check via `{{TRACKER_CLI}} show <id>` for each).
289
- 3. Run integration tests if applicable: `bun test`.
287
+ 3. Run integration tests if applicable: {{QUALITY_GATE_INLINE}}.
290
288
  4. **Record mulch learnings** -- review your orchestration work for insights (decomposition strategies, worker coordination patterns, failures encountered, decisions made) and record them:
291
289
  ```bash
292
290
  ml record <domain> --type <convention|pattern|failure|decision> --description "..."
package/agents/merger.md CHANGED
@@ -11,7 +11,7 @@ Every mail message and every tool call costs tokens. Be concise in communication
11
11
  These are named failures. If you catch yourself doing any of these, stop and correct immediately.
12
12
 
13
13
  - **TIER_SKIP** -- Jumping to a higher resolution tier without first attempting the lower tiers. Always start at Tier 1 and escalate only on failure.
14
- - **UNVERIFIED_MERGE** -- Completing a merge without running `bun test`, `bun run lint`, and `bun run typecheck` to verify the result. A merge that breaks tests is not complete.
14
+ - **UNVERIFIED_MERGE** -- Completing a merge without running {{QUALITY_GATE_INLINE}} to verify the result. A merge that breaks tests is not complete.
15
15
  - **SCOPE_CREEP** -- Modifying code beyond what is needed for conflict resolution. Your job is to merge, not refactor or improve.
16
16
  - **SILENT_FAILURE** -- A merge fails at all tiers and you do not report it via mail. Every unresolvable conflict must be escalated to your parent with `--type error --priority urgent`.
17
17
  - **INCOMPLETE_CLOSE** -- Running `{{TRACKER_CLI}} close` without first verifying tests pass and sending a merge report mail to your parent.
@@ -28,7 +28,7 @@ Your task-specific context (task ID, branches to merge, target branch, merge ord
28
28
  - **Never push to the canonical branch** (main/develop). You commit to your worktree branch only. Merging is handled by the orchestrator or a merger agent.
29
29
  - **Never run `git push`** -- your branch lives in the local worktree. The merge process handles integration.
30
30
  - **Never spawn sub-workers.** You are a leaf node. If you need something decomposed, ask your parent via mail.
31
- - **Run quality gates before closing.** Do not report completion unless `bun test`, `bun run lint`, and `bun run typecheck` pass.
31
+ - **Run quality gates before closing.** Do not report completion unless {{QUALITY_GATE_INLINE}} pass.
32
32
  - If tests fail, fix them. If you cannot fix them, report the failure via mail with `--type error`.
33
33
 
34
34
  ## communication-protocol
@@ -48,9 +48,7 @@ Your task-specific context (task ID, branches to merge, target branch, merge ord
48
48
 
49
49
  ## completion-protocol
50
50
 
51
- 1. Run `bun test` -- all tests must pass after merge.
52
- 2. Run `bun run lint` -- lint must be clean after merge.
53
- 3. Run `bun run typecheck` -- no TypeScript errors after merge.
51
+ {{QUALITY_GATE_STEPS}}
54
52
  4. **Record mulch learnings** -- capture merge resolution insights (conflict patterns, resolution strategies, branch integration issues):
55
53
  ```bash
56
54
  ml record <domain> --type <convention|pattern|failure> --description "..."
@@ -80,9 +78,7 @@ You are a branch integration specialist. When workers complete their tasks on se
80
78
  - `git merge`, `git merge --abort`, `git merge --no-edit`
81
79
  - `git log`, `git diff`, `git show`, `git status`, `git blame`
82
80
  - `git checkout`, `git branch`
83
- - `bun test` (verify merged code passes tests)
84
- - `bun run lint` (verify merged code passes lint)
85
- - `bun run typecheck` (verify no TypeScript errors)
81
+ {{QUALITY_GATE_CAPABILITIES}}
86
82
  - `{{TRACKER_CLI}} show`, `{{TRACKER_CLI}} close` ({{TRACKER_NAME}} task management)
87
83
  - `ml prime`, `ml query` (load expertise for conflict understanding)
88
84
  - `ov merge` (use overstory merge infrastructure)
@@ -135,11 +131,7 @@ If AI-resolve fails or produces broken code:
135
131
  - This is a last resort -- report that reimagine was needed.
136
132
 
137
133
  5. **Verify the merge:**
138
- ```bash
139
- bun test # All tests must pass after merge
140
- bun run lint # Lint must be clean after merge
141
- bun run typecheck # No TypeScript errors after merge
142
- ```
134
+ {{QUALITY_GATE_BASH}}
143
135
  6. **Report the result:**
144
136
  ```bash
145
137
  {{TRACKER_CLI}} close <task-id> --reason "Merged <branch>: <tier used>, tests passing"
@@ -75,10 +75,7 @@ You are a validation specialist. Given code to review, you check it for correctn
75
75
  - **Glob** -- find files by name pattern
76
76
  - **Grep** -- search file contents with regex
77
77
  - **Bash** (observation and test commands only):
78
- - `bun test` (run test suite)
79
- - `bun test <specific-file>` (run targeted tests)
80
- - `bun run lint` (lint and format check)
81
- - `bun run typecheck` (type checking)
78
+ {{QUALITY_GATE_CAPABILITIES}}
82
79
  - `git log`, `git diff`, `git show`, `git blame`
83
80
  - `git diff <base-branch>...<feature-branch>` (review changes)
84
81
  - `{{TRACKER_CLI}} show`, `{{TRACKER_CLI}} ready` (read {{TRACKER_NAME}} state)
@@ -107,11 +104,7 @@ You are a validation specialist. Given code to review, you check it for correctn
107
104
  - Check for: security issues, hardcoded secrets, missing input validation.
108
105
  - Check for: adequate test coverage, meaningful test assertions.
109
106
  5. **Run quality gates:**
110
- ```bash
111
- bun test # Do all tests pass?
112
- bun run lint # Does lint and formatting pass?
113
- bun run typecheck # Are there any TypeScript errors?
114
- ```
107
+ {{QUALITY_GATE_BASH}}
115
108
  6. **Report results** via `{{TRACKER_CLI}} close` with a clear pass/fail summary:
116
109
  ```bash
117
110
  {{TRACKER_CLI}} close <task-id> --reason "PASS: <summary>"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@os-eco/overstory-cli",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
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",
@@ -9,11 +9,13 @@ import {
9
9
  buildPathBoundaryGuardScript,
10
10
  deployHooks,
11
11
  escapeForSingleQuotedShell,
12
+ extractQualityGatePrefixes,
12
13
  getBashPathBoundaryGuards,
13
14
  getCapabilityGuards,
14
15
  getDangerGuards,
15
16
  getPathBoundaryGuards,
16
17
  isOverstoryHookEntry,
18
+ PATH_PREFIX,
17
19
  } from "./hooks-deployer.ts";
18
20
 
19
21
  describe("deployHooks", () => {
@@ -1233,6 +1235,49 @@ describe("getDangerGuards", () => {
1233
1235
  );
1234
1236
  }
1235
1237
  });
1238
+
1239
+ test("custom quality gates appear in safe prefix list for non-implementation capabilities", () => {
1240
+ const guards = getCapabilityGuards("scout", [
1241
+ { name: "Test", command: "pytest", description: "all tests pass" },
1242
+ { name: "Lint", command: "ruff check .", description: "no lint errors" },
1243
+ ]);
1244
+ // Find the Bash guard for file modifications (last Bash entry for non-implementation)
1245
+ const bashGuards = guards.filter((g) => g.matcher === "Bash");
1246
+ const fileGuard = bashGuards.find((g) =>
1247
+ g.hooks.some((h) => h.command.includes("cannot modify files")),
1248
+ );
1249
+ expect(fileGuard).toBeDefined();
1250
+ const command = fileGuard?.hooks[0]?.command ?? "";
1251
+ expect(command).toContain("pytest");
1252
+ expect(command).toContain("ruff check .");
1253
+ // Should NOT contain default bun commands
1254
+ expect(command).not.toContain("bun test");
1255
+ });
1256
+ });
1257
+
1258
+ describe("extractQualityGatePrefixes", () => {
1259
+ test("extracts command from each gate", () => {
1260
+ const gates = [
1261
+ { name: "Test", command: "bun test", description: "all tests pass" },
1262
+ { name: "Lint", command: "bun run lint", description: "zero errors" },
1263
+ ];
1264
+ const prefixes = extractQualityGatePrefixes(gates);
1265
+ expect(prefixes).toEqual(["bun test", "bun run lint"]);
1266
+ });
1267
+
1268
+ test("returns empty array for empty gates", () => {
1269
+ expect(extractQualityGatePrefixes([])).toEqual([]);
1270
+ });
1271
+
1272
+ test("works with non-bun quality gates", () => {
1273
+ const gates = [
1274
+ { name: "Test", command: "pytest", description: "all tests pass" },
1275
+ { name: "Lint", command: "ruff check .", description: "no lint errors" },
1276
+ { name: "Type", command: "mypy src/", description: "type check" },
1277
+ ];
1278
+ const prefixes = extractQualityGatePrefixes(gates);
1279
+ expect(prefixes).toEqual(["pytest", "ruff check .", "mypy src/"]);
1280
+ });
1236
1281
  });
1237
1282
 
1238
1283
  describe("buildBashFileGuardScript", () => {
@@ -1260,6 +1305,14 @@ describe("buildBashFileGuardScript", () => {
1260
1305
  expect(script).toContain("git log");
1261
1306
  expect(script).toContain("git diff");
1262
1307
  expect(script).toContain("mulch ");
1308
+ // Quality gate commands (bun test, bun run lint, etc.) are no longer
1309
+ // hardcoded in SAFE_BASH_PREFIXES — they come from config via
1310
+ // extractQualityGatePrefixes() and are passed as extraSafePrefixes
1311
+ // through getCapabilityGuards().
1312
+ });
1313
+
1314
+ test("includes quality gate prefixes when passed as extraSafePrefixes", () => {
1315
+ const script = buildBashFileGuardScript("scout", ["bun test", "bun run lint"]);
1263
1316
  expect(script).toContain("bun test");
1264
1317
  expect(script).toContain("bun run lint");
1265
1318
  });
@@ -2115,6 +2168,185 @@ describe("bash path boundary integration", () => {
2115
2168
  });
2116
2169
  });
2117
2170
 
2171
+ describe("PATH_PREFIX", () => {
2172
+ test("PATH_PREFIX is exported and is a non-empty string", () => {
2173
+ expect(typeof PATH_PREFIX).toBe("string");
2174
+ expect(PATH_PREFIX.length).toBeGreaterThan(0);
2175
+ });
2176
+
2177
+ test("PATH_PREFIX contains ~/.bun/bin for bun-installed CLIs", () => {
2178
+ expect(PATH_PREFIX).toContain(".bun/bin");
2179
+ });
2180
+
2181
+ test("PATH_PREFIX extends PATH (not replaces it)", () => {
2182
+ // Must preserve original PATH via :$PATH
2183
+ expect(PATH_PREFIX).toContain(":$PATH");
2184
+ });
2185
+
2186
+ test("PATH_PREFIX sets PATH via export", () => {
2187
+ expect(PATH_PREFIX).toMatch(/^export PATH=/);
2188
+ });
2189
+ });
2190
+
2191
+ describe("PATH prefix in deployed hooks", () => {
2192
+ let tempDir: string;
2193
+
2194
+ beforeEach(async () => {
2195
+ tempDir = await mkdtemp(join(tmpdir(), "overstory-path-prefix-test-"));
2196
+ });
2197
+
2198
+ afterEach(async () => {
2199
+ await rm(tempDir, { recursive: true, force: true });
2200
+ });
2201
+
2202
+ test("SessionStart hook commands include PATH prefix", async () => {
2203
+ const worktreePath = join(tempDir, "path-ss-wt");
2204
+ await deployHooks(worktreePath, "path-agent");
2205
+
2206
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2207
+ const parsed = JSON.parse(content);
2208
+ for (const entry of parsed.hooks.SessionStart) {
2209
+ for (const hook of entry.hooks) {
2210
+ expect(hook.command).toContain("export PATH=");
2211
+ expect(hook.command).toContain(".bun/bin");
2212
+ }
2213
+ }
2214
+ });
2215
+
2216
+ test("UserPromptSubmit hook commands include PATH prefix", async () => {
2217
+ const worktreePath = join(tempDir, "path-ups-wt");
2218
+ await deployHooks(worktreePath, "path-agent");
2219
+
2220
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2221
+ const parsed = JSON.parse(content);
2222
+ for (const entry of parsed.hooks.UserPromptSubmit) {
2223
+ for (const hook of entry.hooks) {
2224
+ expect(hook.command).toContain("export PATH=");
2225
+ }
2226
+ }
2227
+ });
2228
+
2229
+ test("PostToolUse hook commands include PATH prefix", async () => {
2230
+ const worktreePath = join(tempDir, "path-ptu-wt");
2231
+ await deployHooks(worktreePath, "path-agent");
2232
+
2233
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2234
+ const parsed = JSON.parse(content);
2235
+ for (const entry of parsed.hooks.PostToolUse) {
2236
+ for (const hook of entry.hooks) {
2237
+ expect(hook.command).toContain("export PATH=");
2238
+ }
2239
+ }
2240
+ });
2241
+
2242
+ test("Stop hook commands include PATH prefix", async () => {
2243
+ const worktreePath = join(tempDir, "path-stop-wt");
2244
+ await deployHooks(worktreePath, "path-agent");
2245
+
2246
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2247
+ const parsed = JSON.parse(content);
2248
+ for (const entry of parsed.hooks.Stop) {
2249
+ for (const hook of entry.hooks) {
2250
+ expect(hook.command).toContain("export PATH=");
2251
+ expect(hook.command).toContain(".bun/bin");
2252
+ }
2253
+ }
2254
+ });
2255
+
2256
+ test("PreCompact hook commands include PATH prefix", async () => {
2257
+ const worktreePath = join(tempDir, "path-pc-wt");
2258
+ await deployHooks(worktreePath, "path-agent");
2259
+
2260
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2261
+ const parsed = JSON.parse(content);
2262
+ for (const entry of parsed.hooks.PreCompact) {
2263
+ for (const hook of entry.hooks) {
2264
+ expect(hook.command).toContain("export PATH=");
2265
+ }
2266
+ }
2267
+ });
2268
+
2269
+ test("PATH prefix appears before CLI command in SessionStart", async () => {
2270
+ const worktreePath = join(tempDir, "path-order-wt");
2271
+ await deployHooks(worktreePath, "path-order-agent");
2272
+
2273
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2274
+ const parsed = JSON.parse(content);
2275
+ const cmd = parsed.hooks.SessionStart[0].hooks[0].command as string;
2276
+ // PATH export must come before the CLI invocation
2277
+ const pathIdx = cmd.indexOf("export PATH=");
2278
+ const ovIdx = cmd.indexOf("ov prime");
2279
+ expect(pathIdx).toBeGreaterThanOrEqual(0);
2280
+ expect(ovIdx).toBeGreaterThan(pathIdx);
2281
+ });
2282
+
2283
+ test("PATH prefix appears before ml learn in Stop hook", async () => {
2284
+ const worktreePath = join(tempDir, "path-ml-wt");
2285
+ await deployHooks(worktreePath, "path-ml-agent");
2286
+
2287
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2288
+ const parsed = JSON.parse(content);
2289
+ const stopHooks = parsed.hooks.Stop[0].hooks;
2290
+ // Second Stop hook is "ml learn"
2291
+ const mlCmd = stopHooks[1].command as string;
2292
+ const pathIdx = mlCmd.indexOf("export PATH=");
2293
+ const mlIdx = mlCmd.indexOf("ml learn");
2294
+ expect(pathIdx).toBeGreaterThanOrEqual(0);
2295
+ expect(mlIdx).toBeGreaterThan(pathIdx);
2296
+ });
2297
+
2298
+ test("generated guard commands do NOT have PATH prefix (they use only built-ins)", async () => {
2299
+ const worktreePath = join(tempDir, "path-guards-wt");
2300
+ await deployHooks(worktreePath, "path-guards-agent", "builder");
2301
+
2302
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2303
+ const parsed = JSON.parse(content);
2304
+ const preToolUse = parsed.hooks.PreToolUse;
2305
+
2306
+ // Path boundary guards (Write/Edit/NotebookEdit) are generated — no PATH prefix
2307
+ const writeGuard = preToolUse.find(
2308
+ (h: { matcher: string; hooks: Array<{ command: string }> }) =>
2309
+ h.matcher === "Write" && h.hooks[0]?.command?.includes("OVERSTORY_WORKTREE_PATH"),
2310
+ );
2311
+ expect(writeGuard).toBeDefined();
2312
+ expect(writeGuard.hooks[0].command).not.toContain("export PATH=");
2313
+
2314
+ // Danger guard (generated) — no PATH prefix
2315
+ const dangerGuard = preToolUse.find(
2316
+ (h: { matcher: string; hooks: Array<{ command: string }> }) =>
2317
+ h.matcher === "Bash" && h.hooks[0]?.command?.includes("git reset --hard"),
2318
+ );
2319
+ expect(dangerGuard).toBeDefined();
2320
+ expect(dangerGuard.hooks[0].command).not.toContain("export PATH=");
2321
+ });
2322
+
2323
+ test("re-deployment is idempotent: PATH prefix not duplicated", async () => {
2324
+ const worktreePath = join(tempDir, "path-idem-wt");
2325
+
2326
+ await deployHooks(worktreePath, "path-idem-agent");
2327
+ await deployHooks(worktreePath, "path-idem-agent");
2328
+
2329
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2330
+ const parsed = JSON.parse(content);
2331
+ const cmd = parsed.hooks.SessionStart[0].hooks[0].command as string;
2332
+
2333
+ // PATH prefix should appear exactly once, not doubled
2334
+ const occurrences = cmd.split("export PATH=").length - 1;
2335
+ expect(occurrences).toBe(1);
2336
+ });
2337
+
2338
+ test("PATH prefix uses $HOME expansion (not hardcoded path)", async () => {
2339
+ const worktreePath = join(tempDir, "path-home-wt");
2340
+ await deployHooks(worktreePath, "home-agent");
2341
+
2342
+ const content = await Bun.file(join(worktreePath, ".claude", "settings.local.json")).text();
2343
+ const parsed = JSON.parse(content);
2344
+ const cmd = parsed.hooks.SessionStart[0].hooks[0].command as string;
2345
+ // Should use $HOME not a hardcoded path like /Users/...
2346
+ expect(cmd).toContain("$HOME");
2347
+ });
2348
+ });
2349
+
2118
2350
  describe("escapeForSingleQuotedShell", () => {
2119
2351
  test("no single quotes: string passes through unchanged", () => {
2120
2352
  expect(escapeForSingleQuotedShell("hello world")).toBe("hello world");
@@ -1,6 +1,8 @@
1
1
  import { mkdir } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
+ import { DEFAULT_QUALITY_GATES } from "../config.ts";
3
4
  import { AgentError } from "../errors.ts";
5
+ import type { QualityGate } from "../types.ts";
4
6
 
5
7
  /**
6
8
  * Capabilities that must never modify project files.
@@ -117,12 +119,20 @@ const SAFE_BASH_PREFIXES = [
117
119
  "git blame",
118
120
  "git branch",
119
121
  "mulch ",
120
- "bun test",
121
- "bun run lint",
122
- "bun run typecheck",
123
- "bun run biome",
124
122
  ];
125
123
 
124
+ /**
125
+ * Extract command prefixes from quality gate configurations.
126
+ *
127
+ * Each gate's command is used as a safe prefix so non-implementation agents
128
+ * can still run quality gate commands (e.g., reviewers running tests).
129
+ * This makes the safe prefix list configurable instead of hardcoding
130
+ * specific tool commands like "bun test".
131
+ */
132
+ export function extractQualityGatePrefixes(gates: QualityGate[]): string[] {
133
+ return gates.map((g) => g.command);
134
+ }
135
+
126
136
  /** Hook entry shape matching Claude Code's settings.local.json format. */
127
137
  interface HookEntry {
128
138
  matcher: string;
@@ -149,6 +159,22 @@ function getTemplatePath(): string {
149
159
  */
150
160
  const ENV_GUARD = '[ -z "$OVERSTORY_AGENT_NAME" ] && exit 0;';
151
161
 
162
+ /**
163
+ * PATH setup prefix for hook commands.
164
+ *
165
+ * Claude Code executes hook commands via /bin/sh with a minimal PATH
166
+ * (/usr/bin:/bin:/usr/sbin:/sbin). Bun-installed CLIs — ov, ml, sd, cn, bd —
167
+ * live in ~/.bun/bin which is absent from that PATH, causing hooks like
168
+ * `ov prime` (SessionStart) and `ml learn` (Stop) to fail with
169
+ * "command not found".
170
+ *
171
+ * Prepend this to any hook command that invokes one of those CLIs so they
172
+ * resolve correctly regardless of how Claude Code was launched.
173
+ *
174
+ * Exported so tests can verify the exact prefix value.
175
+ */
176
+ export const PATH_PREFIX = 'export PATH="$HOME/.bun/bin:/usr/local/bin:/opt/homebrew/bin:$PATH";';
177
+
152
178
  /**
153
179
  * Build a PreToolUse guard script that validates file paths are within
154
180
  * the agent's worktree boundary.
@@ -454,8 +480,10 @@ export function getBashPathBoundaryGuards(): HookEntry[] {
454
480
  *
455
481
  * Note: All capabilities also receive Bash danger guards via getDangerGuards().
456
482
  */
457
- export function getCapabilityGuards(capability: string): HookEntry[] {
483
+ export function getCapabilityGuards(capability: string, qualityGates?: QualityGate[]): HookEntry[] {
458
484
  const guards: HookEntry[] = [];
485
+ const gates = qualityGates ?? DEFAULT_QUALITY_GATES;
486
+ const gatePrefixes = extractQualityGatePrefixes(gates);
459
487
 
460
488
  // Block Claude Code native team/task tools for ALL overstory agents.
461
489
  // Agents must use `overstory sling` for delegation, not native Task/Team tools.
@@ -485,7 +513,9 @@ export function getCapabilityGuards(capability: string): HookEntry[] {
485
513
  guards.push(...toolGuards);
486
514
 
487
515
  // Coordination capabilities get git add/commit whitelisted for beads/mulch sync
488
- const extraSafe = COORDINATION_CAPABILITIES.has(capability) ? COORDINATION_SAFE_PREFIXES : [];
516
+ const extraSafe = COORDINATION_CAPABILITIES.has(capability)
517
+ ? [...COORDINATION_SAFE_PREFIXES, ...gatePrefixes]
518
+ : gatePrefixes;
489
519
  const bashFileGuard: HookEntry = {
490
520
  matcher: "Bash",
491
521
  hooks: [
@@ -544,6 +574,7 @@ export async function deployHooks(
544
574
  worktreePath: string,
545
575
  agentName: string,
546
576
  capability = "builder",
577
+ qualityGates?: QualityGate[],
547
578
  ): Promise<void> {
548
579
  const templatePath = getTemplatePath();
549
580
  const file = Bun.file(templatePath);
@@ -571,11 +602,26 @@ export async function deployHooks(
571
602
  content = content.replace("{{AGENT_NAME}}", agentName);
572
603
  }
573
604
 
574
- // Parse the base config and merge guards into PreToolUse
605
+ // Parse the base config from the template
575
606
  const config = JSON.parse(content) as { hooks: Record<string, HookEntry[]> };
607
+
608
+ // Extend PATH in all template hook commands.
609
+ // Claude Code invokes hooks with PATH=/usr/bin:/bin:/usr/sbin:/sbin — ~/.bun/bin
610
+ // (where ov, ml, sd, etc. live) is not included. Prepend PATH_PREFIX so CLIs resolve.
611
+ for (const entries of Object.values(config.hooks)) {
612
+ for (const entry of entries) {
613
+ for (const hook of entry.hooks) {
614
+ hook.command = `${PATH_PREFIX} ${hook.command}`;
615
+ }
616
+ }
617
+ }
618
+
619
+ // Merge capability-specific PreToolUse guards into the config.
620
+ // Guards are generated scripts using only shell built-ins (grep, sed, echo, exit)
621
+ // and do not require PATH extension.
576
622
  const pathGuards = getPathBoundaryGuards();
577
623
  const dangerGuards = getDangerGuards(agentName);
578
- const capabilityGuards = getCapabilityGuards(capability);
624
+ const capabilityGuards = getCapabilityGuards(capability, qualityGates);
579
625
  const allGuards = [...pathGuards, ...dangerGuards, ...capabilityGuards];
580
626
 
581
627
  if (allGuards.length > 0) {