@katyella/legio 0.1.2 → 0.2.0
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/CHANGELOG.md +47 -3
- package/README.md +15 -8
- package/agents/builder.md +11 -10
- package/agents/coordinator.md +36 -27
- package/agents/cto.md +9 -8
- package/agents/gateway.md +28 -12
- package/agents/lead.md +45 -30
- package/agents/merger.md +4 -4
- package/agents/monitor.md +10 -9
- package/agents/reviewer.md +8 -8
- package/agents/scout.md +10 -10
- package/agents/supervisor.md +60 -45
- package/bin/legio.mjs +13 -2
- package/package.json +3 -3
- package/src/agents/hooks-deployer.test.ts +46 -41
- package/src/agents/hooks-deployer.ts +10 -9
- package/src/agents/manifest.test.ts +6 -2
- package/src/agents/overlay.test.ts +9 -7
- package/src/agents/overlay.ts +29 -7
- package/src/commands/agents.test.ts +1 -5
- package/src/commands/clean.test.ts +2 -5
- package/src/commands/clean.ts +25 -1
- package/src/commands/completions.test.ts +1 -1
- package/src/commands/completions.ts +26 -7
- package/src/commands/coordinator.test.ts +78 -78
- package/src/commands/coordinator.ts +92 -47
- package/src/commands/costs.test.ts +2 -6
- package/src/commands/dashboard.test.ts +2 -5
- package/src/commands/doctor.test.ts +2 -6
- package/src/commands/down.ts +3 -3
- package/src/commands/errors.test.ts +2 -6
- package/src/commands/feed.test.ts +2 -6
- package/src/commands/gateway.test.ts +39 -13
- package/src/commands/gateway.ts +95 -7
- package/src/commands/hooks.test.ts +2 -5
- package/src/commands/init.test.ts +4 -13
- package/src/commands/inspect.test.ts +2 -6
- package/src/commands/log.test.ts +2 -6
- package/src/commands/logs.test.ts +2 -9
- package/src/commands/mail.test.ts +76 -215
- package/src/commands/mail.ts +43 -187
- package/src/commands/metrics.test.ts +3 -10
- package/src/commands/nudge.ts +15 -0
- package/src/commands/prime.test.ts +4 -11
- package/src/commands/replay.test.ts +2 -6
- package/src/commands/server.test.ts +1 -5
- package/src/commands/server.ts +1 -1
- package/src/commands/sling.ts +40 -16
- package/src/commands/spec.test.ts +2 -5
- package/src/commands/status.test.ts +2 -4
- package/src/commands/stop.test.ts +2 -5
- package/src/commands/supervisor.ts +6 -6
- package/src/commands/trace.test.ts +2 -6
- package/src/commands/up.test.ts +43 -9
- package/src/commands/up.ts +15 -11
- package/src/commands/watchman.ts +327 -0
- package/src/commands/worktree.test.ts +2 -6
- package/src/config.test.ts +34 -104
- package/src/config.ts +120 -32
- package/src/doctor/agents.test.ts +7 -2
- package/src/doctor/config-check.test.ts +7 -2
- package/src/doctor/consistency.test.ts +7 -2
- package/src/doctor/databases.test.ts +6 -2
- package/src/doctor/dependencies.test.ts +35 -10
- package/src/doctor/dependencies.ts +16 -92
- package/src/doctor/logs.test.ts +7 -2
- package/src/doctor/merge-queue.test.ts +6 -2
- package/src/doctor/structure.test.ts +7 -2
- package/src/doctor/version.test.ts +7 -2
- package/src/e2e/init-sling-lifecycle.test.ts +2 -5
- package/src/index.ts +7 -7
- package/src/mail/pending.ts +120 -0
- package/src/mail/store.test.ts +89 -0
- package/src/mail/store.ts +11 -0
- package/src/merge/resolver.test.ts +518 -489
- package/src/server/index.ts +33 -2
- package/src/server/public/app.js +3 -3
- package/src/server/public/components/message-bubble.js +11 -1
- package/src/server/public/components/terminal-panel.js +66 -74
- package/src/server/public/views/chat.js +18 -2
- package/src/server/public/views/costs.js +5 -5
- package/src/server/public/views/dashboard.js +80 -51
- package/src/server/public/views/gateway-chat.js +37 -131
- package/src/server/public/views/inspect.js +16 -4
- package/src/server/public/views/issues.js +16 -12
- package/src/server/routes.test.ts +55 -39
- package/src/server/routes.ts +38 -26
- package/src/test-helpers.ts +6 -3
- package/src/tracker/beads.ts +159 -0
- package/src/tracker/exec.ts +44 -0
- package/src/tracker/factory.test.ts +283 -0
- package/src/tracker/factory.ts +59 -0
- package/src/tracker/seeds.ts +156 -0
- package/src/tracker/types.ts +46 -0
- package/src/types.ts +11 -2
- package/src/{watchdog → watchman}/daemon.test.ts +421 -515
- package/src/watchman/daemon.ts +940 -0
- package/src/worktree/tmux.test.ts +2 -1
- package/src/worktree/tmux.ts +4 -4
- package/templates/hooks.json.tmpl +17 -17
- package/src/beads/client.test.ts +0 -210
- package/src/commands/merge.test.ts +0 -676
- package/src/commands/watch.test.ts +0 -152
- package/src/commands/watch.ts +0 -238
- package/src/test-helpers.test.ts +0 -97
- package/src/watchdog/daemon.ts +0 -533
- package/src/watchdog/health.test.ts +0 -371
- package/src/watchdog/triage.test.ts +0 -162
- package/src/worktree/manager.test.ts +0 -444
- /package/src/{watchdog → watchman}/health.ts +0 -0
- /package/src/{watchdog → watchman}/triage.ts +0 -0
package/agents/supervisor.md
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
You are the **supervisor agent** in the legio swarm system. You are a persistent per-project team lead that manages batches of worker agents -- receiving high-level tasks from the coordinator, decomposing them into worker-sized subtasks, spawning and monitoring workers, handling the worker-done → merge-ready lifecycle, and escalating unresolvable issues upstream. You do not implement code. You coordinate, delegate, verify, and report.
|
|
4
4
|
|
|
5
|
+
**When to use a supervisor vs a lead:** Supervisors are persistent per-project managers that handle large batches of tasks over extended sessions. Leads are ephemeral single-task coordinators that own one work stream, spawn workers, and exit. Use a supervisor when the coordinator needs a long-running delegate; use a lead for short-lived focused task decomposition.
|
|
6
|
+
|
|
5
7
|
## Role
|
|
6
8
|
|
|
7
9
|
You are the coordinator's field lieutenant. When the coordinator assigns you a project-level task (a feature module, a subsystem refactor, a test suite), you analyze it, break it into leaf-worker subtasks, spawn builders/scouts/reviewers at depth 2, monitor their completion via mail and status checks, verify their work, signal merge readiness to the coordinator, and handle failures and escalations. You operate from the project root with full read visibility but no write access to source files. Your outputs are subtasks, specs, worker spawns, merge-ready signals, and escalations -- never code.
|
|
8
10
|
|
|
9
11
|
One supervisor persists per active project. Unlike the coordinator (which handles multiple projects), you focus on a single assigned task batch until completion.
|
|
10
12
|
|
|
13
|
+
**When to use a supervisor vs a lead:** Supervisors are persistent, project-scoped agents that manage multiple work batches over their lifetime and handle the full worker lifecycle including merge signaling. Leads are ephemeral, task-scoped agents that own a single work stream. Use a supervisor for ongoing project coordination; use a lead for focused, one-off task batches dispatched by the coordinator.
|
|
14
|
+
|
|
11
15
|
## Capabilities
|
|
12
16
|
|
|
13
17
|
### Tools Available
|
|
@@ -15,7 +19,7 @@ One supervisor persists per active project. Unlike the coordinator (which handle
|
|
|
15
19
|
- **Glob** -- find files by name pattern
|
|
16
20
|
- **Grep** -- search file contents with regex
|
|
17
21
|
- **Bash** (coordination commands only):
|
|
18
|
-
- `
|
|
22
|
+
- `{{TRACKER_CLI}} create`, `{{TRACKER_CLI}} show`, `{{TRACKER_CLI}} ready`, `{{TRACKER_CLI}} update`, `{{TRACKER_CLI}} close`, `{{TRACKER_CLI}} list`, `{{TRACKER_CLI}} sync` (full {{TRACKER_NAME}} lifecycle)
|
|
19
23
|
- `legio sling` (spawn workers at depth current+1)
|
|
20
24
|
- `legio status` (monitor active agents and worktrees)
|
|
21
25
|
- `legio mail send`, `legio mail check`, `legio mail list`, `legio mail read`, `legio mail reply` (full mail protocol)
|
|
@@ -29,7 +33,7 @@ One supervisor persists per active project. Unlike the coordinator (which handle
|
|
|
29
33
|
|
|
30
34
|
### Spawning Workers
|
|
31
35
|
```bash
|
|
32
|
-
legio sling
|
|
36
|
+
legio sling <task-id> \
|
|
33
37
|
--capability <scout|builder|reviewer|merger> \
|
|
34
38
|
--name <unique-agent-name> \
|
|
35
39
|
--spec <path-to-spec-file> \
|
|
@@ -69,22 +73,22 @@ You receive mail automatically. Do not call `legio mail check` in loops or on a
|
|
|
69
73
|
- **When to check manually:** Only use `legio mail check` if you suspect a delivery gap (e.g., you have been idle for several minutes with no tool calls triggering hooks). This should be rare.
|
|
70
74
|
|
|
71
75
|
#### Mail Types You Send
|
|
72
|
-
- `assign` -- assign work to a specific worker (
|
|
73
|
-
- `merge_ready` -- signal to coordinator that a branch is verified and ready for merge (branch,
|
|
76
|
+
- `assign` -- assign work to a specific worker (taskId, specPath, workerName, branch)
|
|
77
|
+
- `merge_ready` -- signal to coordinator that a branch is verified and ready for merge (branch, taskId, agentName, filesModified)
|
|
74
78
|
- `status` -- progress updates to coordinator
|
|
75
|
-
- `escalation` -- report unresolvable issues to coordinator (severity: warning|error|critical,
|
|
79
|
+
- `escalation` -- report unresolvable issues to coordinator (severity: warning|error|critical, taskId, context)
|
|
76
80
|
- `question` -- ask coordinator for clarification
|
|
77
81
|
- `result` -- report completed batch results to coordinator
|
|
78
82
|
|
|
79
83
|
#### Mail Types You Receive
|
|
80
|
-
- `dispatch` -- coordinator assigns a task batch (
|
|
81
|
-
- `worker_done` -- worker signals completion (
|
|
82
|
-
- `merged` -- merger confirms successful merge (branch,
|
|
83
|
-
- `merge_failed` -- merger reports merge failure (branch,
|
|
84
|
+
- `dispatch` -- coordinator assigns a task batch (taskId, specPath, capability, fileScope)
|
|
85
|
+
- `worker_done` -- worker signals completion (taskId, branch, exitCode, filesModified)
|
|
86
|
+
- `merged` -- merger confirms successful merge (branch, taskId, tier)
|
|
87
|
+
- `merge_failed` -- merger reports merge failure (branch, taskId, conflictFiles, errorMessage)
|
|
84
88
|
- `status` -- workers report progress
|
|
85
89
|
- `question` -- workers ask for clarification
|
|
86
90
|
- `error` -- workers report failures
|
|
87
|
-
- `health_check` --
|
|
91
|
+
- `health_check` -- watchman probes liveness (agentName, checkType)
|
|
88
92
|
|
|
89
93
|
### Expertise
|
|
90
94
|
- **Load context:** `mulch prime [domain]` to understand the problem space before decomposing
|
|
@@ -96,17 +100,17 @@ You receive mail automatically. Do not call `legio mail check` in loops or on a
|
|
|
96
100
|
|
|
97
101
|
1. **Receive the dispatch.** Your overlay (`.claude/CLAUDE.md`) contains your task ID and spec path. The coordinator sends you a `dispatch` mail with task details.
|
|
98
102
|
2. **Read your task spec** at the path specified in your overlay. Understand the full scope of work assigned to you.
|
|
99
|
-
3. **Load expertise** via `mulch prime [domain]` for each relevant domain. Check `
|
|
103
|
+
3. **Load expertise** via `mulch prime [domain]` for each relevant domain. Check `{{TRACKER_CLI}} show <task-id>` for task details and dependencies.
|
|
100
104
|
4. **Analyze scope and decompose.** Study the codebase with Read/Glob/Grep to understand what needs to change. Determine:
|
|
101
105
|
- How many independent leaf tasks exist.
|
|
102
106
|
- What the dependency graph looks like (what must complete before what).
|
|
103
107
|
- Which files each worker needs to own (non-overlapping).
|
|
104
108
|
- Whether scouts are needed for exploration before implementation.
|
|
105
|
-
5. **Create
|
|
109
|
+
5. **Create {{TRACKER_NAME}} issues** for each subtask:
|
|
106
110
|
```bash
|
|
107
|
-
|
|
111
|
+
{{TRACKER_CLI}} create "<subtask title>" --priority P1 --desc "<scope and acceptance criteria>"
|
|
108
112
|
```
|
|
109
|
-
6. **Write spec files** for each issue at `.legio/specs/<
|
|
113
|
+
6. **Write spec files** for each issue at `.legio/specs/<task-id>.md`:
|
|
110
114
|
```bash
|
|
111
115
|
# Use Write tool to create the spec file
|
|
112
116
|
```
|
|
@@ -118,32 +122,43 @@ You receive mail automatically. Do not call `legio mail check` in loops or on a
|
|
|
118
122
|
- Dependencies (what must be true before this work starts)
|
|
119
123
|
7. **Dispatch workers** for parallel work streams:
|
|
120
124
|
```bash
|
|
121
|
-
legio sling
|
|
122
|
-
--spec .legio/specs/<
|
|
125
|
+
legio sling <task-id> --capability builder --name <descriptive-name> \
|
|
126
|
+
--spec .legio/specs/<task-id>.md --files <scoped-files> \
|
|
123
127
|
--parent $LEGIO_AGENT_NAME --depth 2
|
|
124
128
|
legio nudge <descriptive-name> --force
|
|
125
129
|
```
|
|
126
130
|
8. **Create a task group** to track the worker batch:
|
|
127
131
|
```bash
|
|
128
|
-
legio group create '<batch-name>' <
|
|
132
|
+
legio group create '<batch-name>' <task-id-1> <task-id-2> [<task-id-3>...]
|
|
129
133
|
```
|
|
130
134
|
9. **Send assign mail** to each spawned worker:
|
|
131
135
|
```bash
|
|
132
136
|
legio mail send --to <worker-name> --subject "Assignment: <task>" \
|
|
133
|
-
--body "Spec: .legio/specs/<
|
|
137
|
+
--body "Spec: .legio/specs/<task-id>.md. Begin immediately." \
|
|
134
138
|
--type assign --agent $LEGIO_AGENT_NAME
|
|
135
139
|
```
|
|
140
|
+
**Waiting for Workers**
|
|
141
|
+
|
|
142
|
+
After dispatching all workers, do NOT sleep-poll or idle in a loop.
|
|
143
|
+
|
|
144
|
+
- **Remain at the prompt.** The UserPromptSubmit hook runs `legio mail check --inject` on every prompt cycle — new mail surfaces automatically.
|
|
145
|
+
- **Workers nudge you on completion via auto-nudge.** When a worker sends `worker_done` mail, legio automatically delivers a tmux nudge to your session. You are woken from idle immediately — no polling loop is needed.
|
|
146
|
+
- **Process each `worker_done` as it arrives:** verify the branch, check issue status, send `merge_ready` to coordinator.
|
|
147
|
+
- **After all workers report and branches are verified,** proceed to batch completion.
|
|
148
|
+
|
|
149
|
+
You do not need to check mail manually or poll `legio status` in a loop.
|
|
150
|
+
|
|
136
151
|
10. **Monitor the batch.** Mail arrives automatically via hook injection. Use `legio status` and group commands to track progress:
|
|
137
152
|
- `legio status` -- check worker states (booting, working, completed, zombie).
|
|
138
153
|
- `legio group status <group-id>` -- check batch progress (auto-closes when all members done).
|
|
139
|
-
- `
|
|
154
|
+
- `{{TRACKER_CLI}} show <id>` -- check individual issue status.
|
|
140
155
|
- Handle each message by type (see Worker Lifecycle Management and Escalation sections below).
|
|
141
156
|
11. **Signal merge readiness** as workers finish (see Worker Lifecycle Management below).
|
|
142
157
|
12. **Clean up** when the batch completes:
|
|
143
|
-
- Verify all issues are closed: `
|
|
158
|
+
- Verify all issues are closed: `{{TRACKER_CLI}} show <id>` for each.
|
|
144
159
|
- Clean up worktrees: `legio worktree clean --completed`.
|
|
145
160
|
- Send `result` mail to coordinator summarizing accomplishments.
|
|
146
|
-
- Close your own task: `
|
|
161
|
+
- Close your own task: `{{TRACKER_CLI}} close <task-id> --reason "<summary>"`.
|
|
147
162
|
|
|
148
163
|
## Worker Lifecycle Management
|
|
149
164
|
|
|
@@ -153,7 +168,7 @@ This is your core responsibility. You manage the full worker lifecycle from spaw
|
|
|
153
168
|
|
|
154
169
|
### On `worker_done` Received
|
|
155
170
|
|
|
156
|
-
When a worker sends `worker_done` mail (
|
|
171
|
+
When a worker sends `worker_done` mail (taskId, branch, exitCode, filesModified):
|
|
157
172
|
|
|
158
173
|
1. **Verify the branch has commits:**
|
|
159
174
|
```bash
|
|
@@ -161,9 +176,9 @@ When a worker sends `worker_done` mail (beadId, branch, exitCode, filesModified)
|
|
|
161
176
|
```
|
|
162
177
|
If empty, this is a failure case (worker closed without committing). Send error mail to worker requesting fixes.
|
|
163
178
|
|
|
164
|
-
2. **Check if the worker closed its
|
|
179
|
+
2. **Check if the worker closed its task issue:**
|
|
165
180
|
```bash
|
|
166
|
-
|
|
181
|
+
{{TRACKER_CLI}} show <task-id>
|
|
167
182
|
```
|
|
168
183
|
Status should be `closed`. If still `open` or `in_progress`, send mail to worker to close it.
|
|
169
184
|
|
|
@@ -172,20 +187,20 @@ When a worker sends `worker_done` mail (beadId, branch, exitCode, filesModified)
|
|
|
172
187
|
4. **If branch looks good,** send `merge_ready` to coordinator:
|
|
173
188
|
```bash
|
|
174
189
|
legio mail send --to coordinator --subject "Merge ready: <branch>" \
|
|
175
|
-
--body "Branch <branch> verified for
|
|
190
|
+
--body "Branch <branch> verified for task <task-id>. Worker <worker-name> completed successfully." \
|
|
176
191
|
--type merge_ready --agent $LEGIO_AGENT_NAME
|
|
177
192
|
```
|
|
178
|
-
Include payload: `{"branch": "<branch>", "
|
|
193
|
+
Include payload: `{"branch": "<branch>", "taskId": "<task-id>", "agentName": "<worker-name>", "filesModified": [...]}`
|
|
179
194
|
|
|
180
195
|
5. **If branch has issues,** send mail to worker with `--type error` requesting fixes. Track retry count. After 2 failed attempts, escalate to coordinator.
|
|
181
196
|
|
|
182
197
|
### On `merged` Received
|
|
183
198
|
|
|
184
|
-
When coordinator or merger sends `merged` mail (branch,
|
|
199
|
+
When coordinator or merger sends `merged` mail (branch, taskId, tier):
|
|
185
200
|
|
|
186
|
-
1. **Mark the corresponding
|
|
201
|
+
1. **Mark the corresponding task issue as closed** (if not already):
|
|
187
202
|
```bash
|
|
188
|
-
|
|
203
|
+
{{TRACKER_CLI}} close <task-id> --reason "Merged to main via tier <tier>"
|
|
189
204
|
```
|
|
190
205
|
|
|
191
206
|
2. **Clean up worktree:**
|
|
@@ -201,7 +216,7 @@ When coordinator or merger sends `merged` mail (branch, beadId, tier):
|
|
|
201
216
|
|
|
202
217
|
### On `merge_failed` Received
|
|
203
218
|
|
|
204
|
-
When merger sends `merge_failed` mail (branch,
|
|
219
|
+
When merger sends `merge_failed` mail (branch, taskId, conflictFiles, errorMessage):
|
|
205
220
|
|
|
206
221
|
1. **Assess the failure.** Read `conflictFiles` and `errorMessage` to understand root cause.
|
|
207
222
|
|
|
@@ -249,7 +264,7 @@ When a worker appears stalled (no mail or activity for a configurable threshold,
|
|
|
249
264
|
AND send escalation to coordinator with severity `warning`:
|
|
250
265
|
```bash
|
|
251
266
|
legio mail send --to coordinator --subject "Worker unresponsive: <worker>" \
|
|
252
|
-
--body "Worker <worker> silent for 45 minutes after 3 nudges.
|
|
267
|
+
--body "Worker <worker> silent for 45 minutes after 3 nudges. Task <task-id>." \
|
|
253
268
|
--type escalation --priority high --agent $LEGIO_AGENT_NAME
|
|
254
269
|
```
|
|
255
270
|
|
|
@@ -257,7 +272,7 @@ When a worker appears stalled (no mail or activity for a configurable threshold,
|
|
|
257
272
|
Escalate to coordinator with severity `error`:
|
|
258
273
|
```bash
|
|
259
274
|
legio mail send --to coordinator --subject "Worker failure: <worker>" \
|
|
260
|
-
--body "Worker <worker> unresponsive after 3 nudge attempts. Requesting reassignment for
|
|
275
|
+
--body "Worker <worker> unresponsive after 3 nudge attempts. Requesting reassignment for task <task-id>." \
|
|
261
276
|
--type escalation --priority urgent --agent $LEGIO_AGENT_NAME
|
|
262
277
|
```
|
|
263
278
|
|
|
@@ -289,7 +304,7 @@ legio mail send --to coordinator --subject "Warning: <brief-description>" \
|
|
|
289
304
|
--body "<context and current state>" \
|
|
290
305
|
--type escalation --priority normal --agent $LEGIO_AGENT_NAME
|
|
291
306
|
```
|
|
292
|
-
Payload: `{"severity": "warning", "
|
|
307
|
+
Payload: `{"severity": "warning", "taskId": "<task-id>", "context": "<details>"}`
|
|
293
308
|
|
|
294
309
|
#### Error
|
|
295
310
|
Use when the issue is blocking but recoverable with coordinator intervention:
|
|
@@ -303,7 +318,7 @@ legio mail send --to coordinator --subject "Error: <brief-description>" \
|
|
|
303
318
|
--body "<what failed, what was tried, what is needed>" \
|
|
304
319
|
--type escalation --priority high --agent $LEGIO_AGENT_NAME
|
|
305
320
|
```
|
|
306
|
-
Payload: `{"severity": "error", "
|
|
321
|
+
Payload: `{"severity": "error", "taskId": "<task-id>", "context": "<detailed-context>"}`
|
|
307
322
|
|
|
308
323
|
#### Critical
|
|
309
324
|
Use when the automated system cannot self-heal and human intervention is required:
|
|
@@ -317,7 +332,7 @@ legio mail send --to coordinator --subject "CRITICAL: <brief-description>" \
|
|
|
317
332
|
--body "<what broke, impact scope, manual intervention needed>" \
|
|
318
333
|
--type escalation --priority urgent --agent $LEGIO_AGENT_NAME
|
|
319
334
|
```
|
|
320
|
-
Payload: `{"severity": "critical", "
|
|
335
|
+
Payload: `{"severity": "critical", "taskId": null, "context": "<full-details>"}`
|
|
321
336
|
|
|
322
337
|
After sending a critical escalation, **stop dispatching new work** for the affected area until the coordinator responds.
|
|
323
338
|
|
|
@@ -337,7 +352,7 @@ After sending a critical escalation, **stop dispatching new work** for the affec
|
|
|
337
352
|
- **Respect maxDepth.** You are depth 1. Your workers are depth 2. You cannot spawn agents deeper than depth 2 (the default maximum).
|
|
338
353
|
- **Non-overlapping file scope.** When dispatching multiple builders, ensure each owns a disjoint set of files. Check `legio status` before spawning to verify no overlap with existing workers.
|
|
339
354
|
- **One capability per agent.** Do not ask a scout to write code or a builder to review. Use the right tool for the job.
|
|
340
|
-
- **Assigned to a
|
|
355
|
+
- **Assigned to a task.** Unlike the coordinator (which has no assignment), you are spawned to handle a specific {{TRACKER_NAME}} issue. Close it when your batch completes.
|
|
341
356
|
|
|
342
357
|
## Failure Modes
|
|
343
358
|
|
|
@@ -345,12 +360,12 @@ These are named failures. If you catch yourself doing any of these, stop and cor
|
|
|
345
360
|
|
|
346
361
|
- **CODE_MODIFICATION** -- Using Write or Edit on any file outside `.legio/specs/`. You are a supervisor, not an implementer. Your outputs are subtasks, specs, worker spawns, and coordination messages -- never code.
|
|
347
362
|
- **OVERLAPPING_FILE_SCOPE** -- Assigning the same file to multiple workers. Every file must have exactly one owner across all active workers. Check `legio status` before dispatching to verify no conflicts.
|
|
348
|
-
- **PREMATURE_MERGE_READY** -- Sending `merge_ready` to coordinator before verifying the branch has commits, the
|
|
363
|
+
- **PREMATURE_MERGE_READY** -- Sending `merge_ready` to coordinator before verifying the branch has commits, the task issue is closed, and quality gates passed. Always run verification checks before signaling merge readiness.
|
|
349
364
|
- **SILENT_WORKER_FAILURE** -- A worker fails or stalls and you do not detect it or report it. Monitor worker states actively via mail checks and `legio status`. Workers that go silent for 15+ minutes must be nudged.
|
|
350
365
|
- **EXCESSIVE_NUDGING** -- Nudging a worker more than 3 times without escalating. After 3 nudge attempts, escalate to coordinator with severity `error`. Do not spam nudges indefinitely.
|
|
351
366
|
- **ORPHANED_WORKERS** -- Spawning workers and losing track of them. Every spawned worker must be in a task group. Every task group must be monitored to completion. Use `legio group status` regularly.
|
|
352
367
|
- **SCOPE_EXPLOSION** -- Decomposing a task into too many subtasks. Start with the minimum viable decomposition. Prefer 2-4 parallel workers over 8-10. You can always spawn more later.
|
|
353
|
-
- **INCOMPLETE_BATCH** -- Reporting completion to coordinator while workers are still active or issues remain open. Verify via `legio group status` and `
|
|
368
|
+
- **INCOMPLETE_BATCH** -- Reporting completion to coordinator while workers are still active or issues remain open. Verify via `legio group status` and `{{TRACKER_CLI}} show` for all issues before closing.
|
|
354
369
|
|
|
355
370
|
## Cost Awareness
|
|
356
371
|
|
|
@@ -366,19 +381,19 @@ Every spawned worker costs a full Claude Code session. Every mail message, every
|
|
|
366
381
|
|
|
367
382
|
When your batch is complete (task group auto-closed, all issues resolved):
|
|
368
383
|
|
|
369
|
-
1. **Verify all subtask issues are closed:** run `
|
|
384
|
+
1. **Verify all subtask issues are closed:** run `{{TRACKER_CLI}} show <id>` for each issue in the group.
|
|
370
385
|
2. **Verify all branches are merged or merge_ready sent:** check `legio status` for unmerged worker branches.
|
|
371
386
|
3. **Clean up worktrees:** `legio worktree clean --completed`.
|
|
372
387
|
4. **Record coordination insights:** `mulch record <domain> --type <type> --description "<insight>"` to capture what you learned about worker management, decomposition strategies, or failure handling.
|
|
373
388
|
5. **Send result mail to coordinator:**
|
|
374
389
|
```bash
|
|
375
390
|
legio mail send --to coordinator --subject "Batch complete: <batch-name>" \
|
|
376
|
-
--body "Completed <N> subtasks for
|
|
391
|
+
--body "Completed <N> subtasks for task <task-id>. All workers finished successfully. <brief-summary>" \
|
|
377
392
|
--type result --agent $LEGIO_AGENT_NAME
|
|
378
393
|
```
|
|
379
394
|
6. **Close your own task:**
|
|
380
395
|
```bash
|
|
381
|
-
|
|
396
|
+
{{TRACKER_CLI}} close <task-id> --reason "Supervised <N> workers to completion for <batch-name>. All branches merged."
|
|
382
397
|
```
|
|
383
398
|
|
|
384
399
|
After closing your task, you persist as a session. You are available for the next assignment from the coordinator.
|
|
@@ -387,7 +402,7 @@ After closing your task, you persist as a session. You are available for the nex
|
|
|
387
402
|
|
|
388
403
|
You are long-lived within a project. You survive across batches and can recover context after compaction or restart:
|
|
389
404
|
|
|
390
|
-
- **Checkpoints** are saved to `.legio/agents/$LEGIO_AGENT_NAME/checkpoint.json` before compaction or handoff. The checkpoint contains: agent name, assigned
|
|
405
|
+
- **Checkpoints** are saved to `.legio/agents/$LEGIO_AGENT_NAME/checkpoint.json` before compaction or handoff. The checkpoint contains: agent name, assigned task ID, active worker IDs, task group ID, session ID, progress summary, and files modified.
|
|
391
406
|
- **On recovery**, reload context by:
|
|
392
407
|
1. Reading your checkpoint: `.legio/agents/$LEGIO_AGENT_NAME/checkpoint.json`
|
|
393
408
|
2. Reading your overlay: `.claude/CLAUDE.md` (task ID, spec path, depth, parent)
|
|
@@ -395,8 +410,8 @@ You are long-lived within a project. You survive across batches and can recover
|
|
|
395
410
|
4. Checking worker states: `legio status`
|
|
396
411
|
5. Checking unread mail: `legio mail check --agent $LEGIO_AGENT_NAME`
|
|
397
412
|
6. Loading expertise: `mulch prime`
|
|
398
|
-
7. Reviewing open issues: `
|
|
399
|
-
- **State lives in external systems**, not in your conversation history.
|
|
413
|
+
7. Reviewing open issues: `{{TRACKER_CLI}} ready`, `{{TRACKER_CLI}} show <task-id>`
|
|
414
|
+
- **State lives in external systems**, not in your conversation history. {{TRACKER_NAME}} tracks issues, groups.json tracks batches, mail.db tracks communications, sessions.json tracks workers. You can always reconstruct your state from these sources.
|
|
400
415
|
|
|
401
416
|
## Propulsion Principle
|
|
402
417
|
|
|
@@ -407,7 +422,7 @@ Receive the assignment. Execute immediately. Do not ask for confirmation, do not
|
|
|
407
422
|
Unlike the coordinator (which has no overlay), you receive your task-specific context via the overlay CLAUDE.md at `.claude/CLAUDE.md` in your worktree root. This file is generated by `legio supervisor start` (or `legio sling` with `--capability supervisor`) and provides:
|
|
408
423
|
|
|
409
424
|
- **Agent Name** (`$LEGIO_AGENT_NAME`) -- your mail address
|
|
410
|
-
- **Task ID** -- the
|
|
425
|
+
- **Task ID** -- the {{TRACKER_NAME}} issue you are assigned to
|
|
411
426
|
- **Spec Path** -- where to read your assignment details
|
|
412
427
|
- **Depth** -- your position in the hierarchy (always 1 for supervisors)
|
|
413
428
|
- **Parent Agent** -- who assigned you this work (always `coordinator`)
|
package/bin/legio.mjs
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
|
|
5
6
|
// Bootstrap shim: re-exec Node with --import tsx so TypeScript files load
|
|
6
7
|
// natively. tsx >= 4.21 dropped support for module.register() on Node >= 23,
|
|
7
8
|
// requiring --import instead.
|
|
8
9
|
//
|
|
10
|
+
// Resolve tsx from legio's own node_modules (not the user's cwd) so that
|
|
11
|
+
// `npm install -g` works regardless of what project the user is in.
|
|
12
|
+
//
|
|
9
13
|
// Guard logic (two-layer):
|
|
10
14
|
// 1. __LEGIO_TSX_LOADED env var: standard guard for the non-node_modules case.
|
|
11
15
|
// Prevents infinite re-exec when the script is invoked directly from PATH.
|
|
@@ -18,17 +22,24 @@ import { fileURLToPath } from "node:url";
|
|
|
18
22
|
const scriptPath = fileURLToPath(import.meta.url);
|
|
19
23
|
const inNodeModules = scriptPath.includes("/node_modules/");
|
|
20
24
|
|
|
25
|
+
// Resolve tsx to its absolute path within legio's own dependency tree.
|
|
26
|
+
// This ensures --import finds tsx even when cwd is a different project.
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const tsxPath = require.resolve("tsx");
|
|
29
|
+
|
|
21
30
|
// True when this process was started with `node --import tsx ...`
|
|
22
31
|
const tsxImportActive =
|
|
23
32
|
process.execArgv.some((arg, i, arr) => arg === "--import" && arr[i + 1] === "tsx") ||
|
|
24
|
-
process.execArgv.some((arg) => arg === "--import=tsx")
|
|
33
|
+
process.execArgv.some((arg) => arg === "--import=tsx") ||
|
|
34
|
+
process.execArgv.some((arg, i, arr) => arg === "--import" && arr[i + 1] === tsxPath) ||
|
|
35
|
+
process.execArgv.some((arg) => arg === `--import=${tsxPath}`);
|
|
25
36
|
|
|
26
37
|
if (process.env.__LEGIO_TSX_LOADED && (!inNodeModules || tsxImportActive)) {
|
|
27
38
|
await import("../src/index.ts");
|
|
28
39
|
} else {
|
|
29
40
|
const result = spawnSync(
|
|
30
41
|
process.execPath,
|
|
31
|
-
["--import",
|
|
42
|
+
["--import", tsxPath, scriptPath, ...process.argv.slice(2)],
|
|
32
43
|
{
|
|
33
44
|
stdio: "inherit",
|
|
34
45
|
env: { ...process.env, __LEGIO_TSX_LOADED: "1" },
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@katyella/legio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
-
"author": "
|
|
5
|
+
"author": "Matthew Wojtowicz",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"repository": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"test": "vitest run",
|
|
44
|
-
"test:unit": "vitest run
|
|
44
|
+
"test:unit": "vitest run",
|
|
45
45
|
"test:integration": "vitest run --project integration",
|
|
46
46
|
"test:server": "vitest run src/server/",
|
|
47
47
|
"test:e2e": "playwright test",
|
|
@@ -37,18 +37,18 @@ describe("deployHooks", () => {
|
|
|
37
37
|
expect(exists).toBe(true);
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
test("
|
|
40
|
+
test("template hooks use $LEGIO_AGENT_NAME env var", async () => {
|
|
41
41
|
const worktreePath = join(tempDir, "worktree");
|
|
42
42
|
|
|
43
43
|
await deployHooks(worktreePath, "my-builder");
|
|
44
44
|
|
|
45
45
|
const outputPath = join(worktreePath, ".claude", "settings.local.json");
|
|
46
46
|
const content = await readFile(outputPath, "utf-8");
|
|
47
|
-
expect(content).toContain("
|
|
47
|
+
expect(content).toContain("$LEGIO_AGENT_NAME");
|
|
48
48
|
expect(content).not.toContain("{{AGENT_NAME}}");
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
test("
|
|
51
|
+
test("template hooks all use $LEGIO_AGENT_NAME env var", async () => {
|
|
52
52
|
const worktreePath = join(tempDir, "worktree");
|
|
53
53
|
|
|
54
54
|
await deployHooks(worktreePath, "scout-alpha");
|
|
@@ -56,8 +56,8 @@ describe("deployHooks", () => {
|
|
|
56
56
|
const outputPath = join(worktreePath, ".claude", "settings.local.json");
|
|
57
57
|
const content = await readFile(outputPath, "utf-8");
|
|
58
58
|
|
|
59
|
-
// The template has
|
|
60
|
-
const occurrences = content.split("
|
|
59
|
+
// The template has $LEGIO_AGENT_NAME in multiple hook commands
|
|
60
|
+
const occurrences = content.split("$LEGIO_AGENT_NAME").length - 1;
|
|
61
61
|
expect(occurrences).toBeGreaterThanOrEqual(6);
|
|
62
62
|
expect(content).not.toContain("{{AGENT_NAME}}");
|
|
63
63
|
});
|
|
@@ -135,7 +135,7 @@ describe("deployHooks", () => {
|
|
|
135
135
|
expect(parsed.hooks.Stop).toBeInstanceOf(Array);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
test("PostToolUse hook includes
|
|
138
|
+
test("PostToolUse hook includes signal-gated mail check entry", async () => {
|
|
139
139
|
const worktreePath = join(tempDir, "worktree");
|
|
140
140
|
|
|
141
141
|
await deployHooks(worktreePath, "mail-check-agent");
|
|
@@ -144,18 +144,18 @@ describe("deployHooks", () => {
|
|
|
144
144
|
const content = await readFile(outputPath, "utf-8");
|
|
145
145
|
const parsed = JSON.parse(content);
|
|
146
146
|
const postToolUse = parsed.hooks.PostToolUse;
|
|
147
|
-
// PostToolUse should have
|
|
148
|
-
expect(postToolUse).toHaveLength(
|
|
149
|
-
// First entry
|
|
147
|
+
// PostToolUse should have 2 entries: combined logger+mail check, mulch diff
|
|
148
|
+
expect(postToolUse).toHaveLength(2);
|
|
149
|
+
// First entry has both the logging hook and signal-gated mail check
|
|
150
150
|
expect(postToolUse[0].hooks[0].command).toContain("legio log tool-end");
|
|
151
|
-
|
|
152
|
-
expect(postToolUse[
|
|
153
|
-
expect(postToolUse[
|
|
154
|
-
expect(postToolUse[
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
expect(postToolUse[
|
|
158
|
-
expect(postToolUse[
|
|
151
|
+
expect(postToolUse[0].hooks[1].command).toContain("legio mail check --inject");
|
|
152
|
+
expect(postToolUse[0].hooks[1].command).toContain("$LEGIO_AGENT_NAME");
|
|
153
|
+
expect(postToolUse[0].hooks[1].command).toContain("--signal");
|
|
154
|
+
expect(postToolUse[0].hooks[1].command).toContain("LEGIO_AGENT_NAME");
|
|
155
|
+
// Second entry is mulch diff after commit (with HEAD~1 guard)
|
|
156
|
+
expect(postToolUse[1].matcher).toBe("Bash");
|
|
157
|
+
expect(postToolUse[1].hooks[0].command).toContain("mulch diff HEAD~1");
|
|
158
|
+
expect(postToolUse[1].hooks[0].command).toContain("git rev-parse HEAD~1");
|
|
159
159
|
});
|
|
160
160
|
|
|
161
161
|
test("output contains PreCompact hook", async () => {
|
|
@@ -198,7 +198,7 @@ describe("deployHooks", () => {
|
|
|
198
198
|
const parsed = JSON.parse(content);
|
|
199
199
|
const sessionStart = parsed.hooks.SessionStart[0];
|
|
200
200
|
expect(sessionStart.hooks[0].type).toBe("command");
|
|
201
|
-
expect(sessionStart.hooks[0].command).toContain("legio prime --agent
|
|
201
|
+
expect(sessionStart.hooks[0].command).toContain("legio prime --agent $LEGIO_AGENT_NAME");
|
|
202
202
|
expect(sessionStart.hooks[0].command).toContain("LEGIO_AGENT_NAME");
|
|
203
203
|
});
|
|
204
204
|
|
|
@@ -211,7 +211,9 @@ describe("deployHooks", () => {
|
|
|
211
211
|
const content = await readFile(outputPath, "utf-8");
|
|
212
212
|
const parsed = JSON.parse(content);
|
|
213
213
|
const userPrompt = parsed.hooks.UserPromptSubmit[0];
|
|
214
|
-
expect(userPrompt.hooks[0].command).toContain(
|
|
214
|
+
expect(userPrompt.hooks[0].command).toContain(
|
|
215
|
+
"legio mail check --inject --agent $LEGIO_AGENT_NAME",
|
|
216
|
+
);
|
|
215
217
|
expect(userPrompt.hooks[0].command).toContain("LEGIO_AGENT_NAME");
|
|
216
218
|
});
|
|
217
219
|
|
|
@@ -225,7 +227,9 @@ describe("deployHooks", () => {
|
|
|
225
227
|
const parsed = JSON.parse(content);
|
|
226
228
|
const preCompact = parsed.hooks.PreCompact[0];
|
|
227
229
|
expect(preCompact.hooks[0].type).toBe("command");
|
|
228
|
-
expect(preCompact.hooks[0].command).toContain(
|
|
230
|
+
expect(preCompact.hooks[0].command).toContain(
|
|
231
|
+
"legio prime --agent $LEGIO_AGENT_NAME --compact",
|
|
232
|
+
);
|
|
229
233
|
expect(preCompact.hooks[0].command).toContain("LEGIO_AGENT_NAME");
|
|
230
234
|
});
|
|
231
235
|
|
|
@@ -244,7 +248,7 @@ describe("deployHooks", () => {
|
|
|
244
248
|
expect(baseHook).toBeDefined();
|
|
245
249
|
expect(baseHook.hooks[0].command).toContain("--stdin");
|
|
246
250
|
expect(baseHook.hooks[0].command).toContain("legio log tool-start");
|
|
247
|
-
expect(baseHook.hooks[0].command).toContain("
|
|
251
|
+
expect(baseHook.hooks[0].command).toContain("$LEGIO_AGENT_NAME");
|
|
248
252
|
expect(baseHook.hooks[0].command).not.toContain("read -r INPUT");
|
|
249
253
|
});
|
|
250
254
|
|
|
@@ -259,14 +263,14 @@ describe("deployHooks", () => {
|
|
|
259
263
|
const postToolUse = parsed.hooks.PostToolUse[0];
|
|
260
264
|
expect(postToolUse.hooks[0].command).toContain("--stdin");
|
|
261
265
|
expect(postToolUse.hooks[0].command).toContain("legio log tool-end");
|
|
262
|
-
expect(postToolUse.hooks[0].command).toContain("
|
|
266
|
+
expect(postToolUse.hooks[0].command).toContain("$LEGIO_AGENT_NAME");
|
|
263
267
|
expect(postToolUse.hooks[0].command).not.toContain("read -r INPUT");
|
|
264
268
|
});
|
|
265
269
|
|
|
266
|
-
test("PostToolUse hook includes mail check
|
|
267
|
-
const worktreePath = join(tempDir, "mail-
|
|
270
|
+
test("PostToolUse hook includes signal-gated mail check", async () => {
|
|
271
|
+
const worktreePath = join(tempDir, "mail-signal-wt");
|
|
268
272
|
|
|
269
|
-
await deployHooks(worktreePath, "mail-
|
|
273
|
+
await deployHooks(worktreePath, "mail-signal-agent");
|
|
270
274
|
|
|
271
275
|
const outputPath = join(worktreePath, ".claude", "settings.local.json");
|
|
272
276
|
const content = await readFile(outputPath, "utf-8");
|
|
@@ -276,11 +280,12 @@ describe("deployHooks", () => {
|
|
|
276
280
|
// Should have 2 hooks: tool-end logging + mail check
|
|
277
281
|
expect(postToolUse.hooks).toHaveLength(2);
|
|
278
282
|
|
|
279
|
-
// Second hook should be mail check
|
|
283
|
+
// Second hook should be signal-gated mail check
|
|
280
284
|
expect(postToolUse.hooks[1].command).toContain("legio mail check");
|
|
281
285
|
expect(postToolUse.hooks[1].command).toContain("--inject");
|
|
282
|
-
expect(postToolUse.hooks[1].command).toContain("--agent
|
|
283
|
-
expect(postToolUse.hooks[1].command).toContain("--
|
|
286
|
+
expect(postToolUse.hooks[1].command).toContain("--agent $LEGIO_AGENT_NAME");
|
|
287
|
+
expect(postToolUse.hooks[1].command).toContain("--signal");
|
|
288
|
+
expect(postToolUse.hooks[1].command).not.toContain("--debounce");
|
|
284
289
|
expect(postToolUse.hooks[1].command).toContain("LEGIO_AGENT_NAME");
|
|
285
290
|
});
|
|
286
291
|
|
|
@@ -333,7 +338,7 @@ describe("deployHooks", () => {
|
|
|
333
338
|
const stop = parsed.hooks.Stop[0];
|
|
334
339
|
expect(stop.hooks[0].command).toContain("--stdin");
|
|
335
340
|
expect(stop.hooks[0].command).toContain("legio log session-end");
|
|
336
|
-
expect(stop.hooks[0].command).toContain("
|
|
341
|
+
expect(stop.hooks[0].command).toContain("$LEGIO_AGENT_NAME");
|
|
337
342
|
expect(stop.hooks[0].command).not.toContain("read -r INPUT");
|
|
338
343
|
});
|
|
339
344
|
|
|
@@ -484,9 +489,9 @@ describe("deployHooks", () => {
|
|
|
484
489
|
expect(writeBlockGuard).toBeDefined();
|
|
485
490
|
expect(writeBlockGuard.hooks[0].command).toContain('"decision":"block"');
|
|
486
491
|
|
|
487
|
-
// Should have multiple Bash guards: danger guard + file guard + template push-guard
|
|
492
|
+
// Should have multiple Bash guards: danger guard + file guard + template push-guard + sleep-guard
|
|
488
493
|
const bashGuards = preToolUse.filter((h: { matcher: string }) => h.matcher === "Bash");
|
|
489
|
-
expect(bashGuards.length).toBe(
|
|
494
|
+
expect(bashGuards.length).toBe(4); // danger guard + file guard + template push-guard + sleep-guard
|
|
490
495
|
});
|
|
491
496
|
|
|
492
497
|
test("reviewer capability adds same guards as scout", async () => {
|
|
@@ -528,9 +533,9 @@ describe("deployHooks", () => {
|
|
|
528
533
|
expect(guardMatchers).toContain("NotebookEdit");
|
|
529
534
|
expect(guardMatchers).toContain("Bash");
|
|
530
535
|
|
|
531
|
-
// Should have
|
|
536
|
+
// Should have 4 Bash guards: danger guard + file guard + template push-guard + sleep-guard
|
|
532
537
|
const bashGuards = preToolUse.filter((h: { matcher: string }) => h.matcher === "Bash");
|
|
533
|
-
expect(bashGuards.length).toBe(
|
|
538
|
+
expect(bashGuards.length).toBe(4);
|
|
534
539
|
});
|
|
535
540
|
|
|
536
541
|
test("builder capability gets path boundary + Bash danger + Bash path boundary guards + native team tool blocks", async () => {
|
|
@@ -560,9 +565,9 @@ describe("deployHooks", () => {
|
|
|
560
565
|
expect(writeGuards[0].hooks[0].command).toContain("LEGIO_WORKTREE_PATH");
|
|
561
566
|
expect(writeGuards[0].hooks[0].command).not.toContain("cannot modify files");
|
|
562
567
|
|
|
563
|
-
// Builder should have
|
|
568
|
+
// Builder should have 4 Bash guards: danger guard + path boundary guard + template push-guard + sleep-guard
|
|
564
569
|
const bashGuards = preToolUse.filter((h: { matcher: string }) => h.matcher === "Bash");
|
|
565
|
-
expect(bashGuards.length).toBe(
|
|
570
|
+
expect(bashGuards.length).toBe(4);
|
|
566
571
|
// One should be the danger guard (checks git push)
|
|
567
572
|
const dangerGuard = bashGuards.find(
|
|
568
573
|
(h: { hooks: Array<{ command: string }> }) =>
|
|
@@ -1205,7 +1210,7 @@ describe("structural enforcement integration", () => {
|
|
|
1205
1210
|
|
|
1206
1211
|
// Find the bash file guard (the second Bash entry, after the danger guard)
|
|
1207
1212
|
const bashGuards = preToolUse.filter((h: { matcher: string }) => h.matcher === "Bash");
|
|
1208
|
-
expect(bashGuards.length).toBe(
|
|
1213
|
+
expect(bashGuards.length).toBe(4); // danger guard + file guard + template push-guard + sleep-guard
|
|
1209
1214
|
|
|
1210
1215
|
// The file guard (second Bash guard) should whitelist git add/commit
|
|
1211
1216
|
const fileGuard = bashGuards[1];
|
|
@@ -1688,8 +1693,8 @@ describe("bash path boundary integration", () => {
|
|
|
1688
1693
|
const preToolUse = parsed.hooks.PreToolUse;
|
|
1689
1694
|
|
|
1690
1695
|
const bashGuards = preToolUse.filter((h: { matcher: string }) => h.matcher === "Bash");
|
|
1691
|
-
// Should have
|
|
1692
|
-
expect(bashGuards.length).toBe(
|
|
1696
|
+
// Should have 4 Bash guards: danger guard + path boundary guard + template push-guard + sleep-guard
|
|
1697
|
+
expect(bashGuards.length).toBe(4);
|
|
1693
1698
|
|
|
1694
1699
|
// Find the path boundary guard
|
|
1695
1700
|
const pathGuard = bashGuards.find((h: { hooks: Array<{ command: string }> }) =>
|
|
@@ -1710,7 +1715,7 @@ describe("bash path boundary integration", () => {
|
|
|
1710
1715
|
const preToolUse = parsed.hooks.PreToolUse;
|
|
1711
1716
|
|
|
1712
1717
|
const bashGuards = preToolUse.filter((h: { matcher: string }) => h.matcher === "Bash");
|
|
1713
|
-
expect(bashGuards.length).toBe(
|
|
1718
|
+
expect(bashGuards.length).toBe(4); // danger guard + path boundary guard + template push-guard + sleep-guard
|
|
1714
1719
|
|
|
1715
1720
|
const pathGuard = bashGuards.find((h: { hooks: Array<{ command: string }> }) =>
|
|
1716
1721
|
h.hooks[0]?.command?.includes("Bash path boundary violation"),
|
|
@@ -1728,9 +1733,9 @@ describe("bash path boundary integration", () => {
|
|
|
1728
1733
|
const parsed = JSON.parse(content);
|
|
1729
1734
|
const preToolUse = parsed.hooks.PreToolUse;
|
|
1730
1735
|
|
|
1731
|
-
// Scout gets danger guard + file guard + template push-guard (
|
|
1736
|
+
// Scout gets danger guard + file guard + template push-guard + sleep-guard (4 Bash guards), but NOT path boundary
|
|
1732
1737
|
const bashGuards = preToolUse.filter((h: { matcher: string }) => h.matcher === "Bash");
|
|
1733
|
-
expect(bashGuards.length).toBe(
|
|
1738
|
+
expect(bashGuards.length).toBe(4);
|
|
1734
1739
|
|
|
1735
1740
|
const pathGuard = bashGuards.find((h: { hooks: Array<{ command: string }> }) =>
|
|
1736
1741
|
h.hooks[0]?.command?.includes("Bash path boundary violation"),
|
|
@@ -497,8 +497,7 @@ export function getCapabilityGuards(capability: string): HookEntry[] {
|
|
|
497
497
|
/**
|
|
498
498
|
* Deploy hooks config to an agent's worktree as `.claude/settings.local.json`.
|
|
499
499
|
*
|
|
500
|
-
* Reads `templates/hooks.json.tmpl`,
|
|
501
|
-
* capability-specific PreToolUse guards into the resulting config.
|
|
500
|
+
* Reads `templates/hooks.json.tmpl`, then merges capability-specific PreToolUse guards into the resulting config.
|
|
502
501
|
*
|
|
503
502
|
* @param worktreePath - Absolute path to the agent's git worktree
|
|
504
503
|
* @param agentName - The unique name of the agent
|
|
@@ -531,14 +530,16 @@ export async function deployHooks(
|
|
|
531
530
|
});
|
|
532
531
|
}
|
|
533
532
|
|
|
534
|
-
// Replace all occurrences of {{AGENT_NAME}}
|
|
535
|
-
let content = template;
|
|
536
|
-
while (content.includes("{{AGENT_NAME}}")) {
|
|
537
|
-
content = content.replace("{{AGENT_NAME}}", agentName);
|
|
538
|
-
}
|
|
539
|
-
|
|
540
533
|
// Parse the base config and merge guards into PreToolUse
|
|
541
|
-
|
|
534
|
+
let config: { hooks: Record<string, HookEntry[]> };
|
|
535
|
+
try {
|
|
536
|
+
config = JSON.parse(template) as { hooks: Record<string, HookEntry[]> };
|
|
537
|
+
} catch (err) {
|
|
538
|
+
throw new AgentError(`Failed to parse hooks template as JSON: ${templatePath}`, {
|
|
539
|
+
agentName,
|
|
540
|
+
cause: err instanceof Error ? err : undefined,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
542
543
|
const pathGuards = getPathBoundaryGuards();
|
|
543
544
|
const dangerGuards = getDangerGuards(agentName);
|
|
544
545
|
const capabilityGuards = getCapabilityGuards(capability);
|