@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.
- package/README.md +19 -5
- package/agents/builder.md +6 -15
- package/agents/lead.md +4 -6
- package/agents/merger.md +5 -13
- package/agents/reviewer.md +2 -9
- package/package.json +1 -1
- package/src/agents/hooks-deployer.test.ts +232 -0
- package/src/agents/hooks-deployer.ts +54 -8
- package/src/agents/overlay.test.ts +156 -1
- package/src/agents/overlay.ts +67 -7
- package/src/commands/agents.ts +9 -6
- package/src/commands/clean.ts +2 -1
- package/src/commands/completions.test.ts +8 -20
- package/src/commands/completions.ts +7 -6
- package/src/commands/coordinator.test.ts +8 -0
- package/src/commands/coordinator.ts +11 -8
- 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 +96 -51
- package/src/commands/ecosystem.ts +291 -0
- 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 +5 -4
- package/src/commands/group.ts +23 -14
- package/src/commands/hooks.ts +2 -1
- package/src/commands/init.test.ts +104 -0
- package/src/commands/init.ts +11 -7
- 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 +11 -10
- package/src/commands/mail.ts +11 -12
- package/src/commands/merge.ts +11 -12
- package/src/commands/metrics.test.ts +15 -2
- package/src/commands/metrics.ts +3 -2
- package/src/commands/monitor.ts +5 -4
- package/src/commands/nudge.ts +2 -3
- 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 +17 -20
- package/src/commands/sling.ts +3 -2
- package/src/commands/status.test.ts +2 -1
- package/src/commands/status.ts +7 -6
- package/src/commands/stop.test.ts +2 -0
- package/src/commands/stop.ts +10 -11
- package/src/commands/supervisor.ts +7 -6
- package/src/commands/trace.test.ts +52 -44
- package/src/commands/trace.ts +5 -4
- package/src/commands/upgrade.test.ts +46 -0
- package/src/commands/upgrade.ts +259 -0
- package/src/commands/watch.ts +8 -10
- package/src/commands/worktree.test.ts +21 -15
- package/src/commands/worktree.ts +10 -4
- package/src/doctor/databases.test.ts +38 -0
- package/src/doctor/databases.ts +7 -10
- package/src/doctor/ecosystem.test.ts +307 -0
- package/src/doctor/ecosystem.ts +155 -0
- package/src/doctor/merge-queue.test.ts +98 -0
- package/src/doctor/merge-queue.ts +23 -0
- package/src/doctor/structure.test.ts +130 -1
- package/src/doctor/structure.ts +87 -1
- package/src/doctor/types.ts +5 -2
- 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` (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
18
|
-
- **MISSING_WORKER_DONE** -- Closing a
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 (
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
package/agents/reviewer.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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)
|
|
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
|
|
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) {
|