@os-eco/overstory-cli 0.6.1 → 0.6.4
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 +7 -6
- package/package.json +12 -4
- package/src/agents/hooks-deployer.test.ts +94 -16
- package/src/agents/hooks-deployer.ts +18 -0
- package/src/agents/manifest.test.ts +86 -0
- package/src/commands/agents.test.ts +3 -3
- package/src/commands/agents.ts +59 -88
- package/src/commands/clean.test.ts +31 -46
- package/src/commands/clean.ts +28 -49
- package/src/commands/completions.ts +14 -0
- package/src/commands/coordinator.test.ts +131 -24
- package/src/commands/coordinator.ts +100 -63
- package/src/commands/costs.test.ts +2 -2
- package/src/commands/costs.ts +96 -75
- package/src/commands/dashboard.test.ts +2 -2
- package/src/commands/dashboard.ts +73 -93
- package/src/commands/doctor.test.ts +2 -2
- package/src/commands/doctor.ts +92 -79
- package/src/commands/errors.test.ts +2 -2
- package/src/commands/errors.ts +56 -50
- package/src/commands/feed.test.ts +2 -2
- package/src/commands/feed.ts +86 -83
- package/src/commands/group.ts +167 -177
- package/src/commands/hooks.test.ts +2 -2
- package/src/commands/hooks.ts +52 -42
- package/src/commands/init.test.ts +19 -19
- package/src/commands/init.ts +7 -16
- package/src/commands/inspect.test.ts +2 -2
- package/src/commands/inspect.ts +54 -57
- package/src/commands/log.test.ts +5 -10
- package/src/commands/log.ts +90 -84
- package/src/commands/logs.test.ts +1 -1
- package/src/commands/logs.ts +101 -104
- package/src/commands/mail.ts +157 -169
- package/src/commands/merge.test.ts +20 -58
- package/src/commands/merge.ts +13 -43
- package/src/commands/metrics.test.ts +2 -2
- package/src/commands/metrics.ts +33 -34
- package/src/commands/monitor.test.ts +3 -3
- package/src/commands/monitor.ts +56 -61
- package/src/commands/nudge.ts +41 -89
- package/src/commands/prime.test.ts +15 -47
- package/src/commands/prime.ts +7 -44
- package/src/commands/replay.test.ts +2 -2
- package/src/commands/replay.ts +79 -86
- package/src/commands/run.ts +97 -77
- package/src/commands/sling.test.ts +196 -0
- package/src/commands/sling.ts +24 -54
- package/src/commands/spec.test.ts +13 -39
- package/src/commands/spec.ts +30 -99
- package/src/commands/status.ts +46 -42
- package/src/commands/stop.test.ts +21 -39
- package/src/commands/stop.ts +18 -33
- package/src/commands/supervisor.test.ts +3 -5
- package/src/commands/supervisor.ts +136 -157
- package/src/commands/trace.test.ts +9 -9
- package/src/commands/trace.ts +54 -77
- package/src/commands/watch.test.ts +2 -2
- package/src/commands/watch.ts +38 -45
- package/src/commands/worktree.test.ts +8 -8
- package/src/commands/worktree.ts +63 -46
- package/src/config.test.ts +96 -0
- package/src/doctor/databases.test.ts +22 -2
- package/src/doctor/databases.ts +16 -0
- package/src/doctor/dependencies.test.ts +55 -1
- package/src/doctor/dependencies.ts +113 -18
- package/src/e2e/init-sling-lifecycle.test.ts +6 -6
- package/src/index.ts +223 -213
- package/src/logging/color.test.ts +74 -91
- package/src/logging/color.ts +52 -46
- package/src/logging/reporter.test.ts +10 -10
- package/src/logging/reporter.ts +6 -5
- package/src/merge/queue.test.ts +66 -0
- package/src/merge/queue.ts +15 -0
- package/src/schema-consistency.test.ts +239 -0
- package/src/sessions/compat.ts +1 -1
- package/src/sessions/store.test.ts +37 -0
- package/src/sessions/store.ts +11 -0
- package/src/worktree/tmux.test.ts +98 -9
- package/src/worktree/tmux.ts +18 -0
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { mkdir } from "node:fs/promises";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
+
import { Command } from "commander";
|
|
17
18
|
import { deployHooks } from "../agents/hooks-deployer.ts";
|
|
18
19
|
import { createIdentity, loadIdentity } from "../agents/identity.ts";
|
|
19
20
|
import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
|
|
@@ -56,58 +57,6 @@ export function buildSupervisorBeacon(opts: {
|
|
|
56
57
|
return parts.join(" — ");
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
/**
|
|
60
|
-
* Parse flags from command args.
|
|
61
|
-
*/
|
|
62
|
-
function parseFlags(args: string[]): {
|
|
63
|
-
task: string | null;
|
|
64
|
-
name: string | null;
|
|
65
|
-
parent: string;
|
|
66
|
-
depth: number;
|
|
67
|
-
json: boolean;
|
|
68
|
-
} {
|
|
69
|
-
const flags = {
|
|
70
|
-
task: null as string | null,
|
|
71
|
-
name: null as string | null,
|
|
72
|
-
parent: "coordinator",
|
|
73
|
-
depth: 1,
|
|
74
|
-
json: false,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
for (let i = 0; i < args.length; i++) {
|
|
78
|
-
const arg = args[i];
|
|
79
|
-
if (arg === "--task" && i + 1 < args.length) {
|
|
80
|
-
const val = args[i + 1];
|
|
81
|
-
if (val !== undefined) {
|
|
82
|
-
flags.task = val;
|
|
83
|
-
}
|
|
84
|
-
i++;
|
|
85
|
-
} else if (arg === "--name" && i + 1 < args.length) {
|
|
86
|
-
const val = args[i + 1];
|
|
87
|
-
if (val !== undefined) {
|
|
88
|
-
flags.name = val;
|
|
89
|
-
}
|
|
90
|
-
i++;
|
|
91
|
-
} else if (arg === "--parent" && i + 1 < args.length) {
|
|
92
|
-
const val = args[i + 1];
|
|
93
|
-
if (val !== undefined) {
|
|
94
|
-
flags.parent = val;
|
|
95
|
-
}
|
|
96
|
-
i++;
|
|
97
|
-
} else if (arg === "--depth" && i + 1 < args.length) {
|
|
98
|
-
const val = args[i + 1];
|
|
99
|
-
if (val !== undefined) {
|
|
100
|
-
flags.depth = Number.parseInt(val, 10);
|
|
101
|
-
}
|
|
102
|
-
i++;
|
|
103
|
-
} else if (arg === "--json") {
|
|
104
|
-
flags.json = true;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return flags;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
60
|
/**
|
|
112
61
|
* Start a supervisor agent.
|
|
113
62
|
*
|
|
@@ -121,19 +70,23 @@ function parseFlags(args: string[]): {
|
|
|
121
70
|
* 8. Send startup beacon
|
|
122
71
|
* 9. Record session in SessionStore (sessions.db)
|
|
123
72
|
*/
|
|
124
|
-
async function startSupervisor(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
73
|
+
async function startSupervisor(opts: {
|
|
74
|
+
task: string;
|
|
75
|
+
name: string;
|
|
76
|
+
parent: string;
|
|
77
|
+
depth: number;
|
|
78
|
+
json: boolean;
|
|
79
|
+
}): Promise<void> {
|
|
80
|
+
if (!opts.task) {
|
|
128
81
|
throw new ValidationError("--task <bead-id> is required", {
|
|
129
82
|
field: "task",
|
|
130
|
-
value:
|
|
83
|
+
value: opts.task,
|
|
131
84
|
});
|
|
132
85
|
}
|
|
133
|
-
if (!
|
|
86
|
+
if (!opts.name) {
|
|
134
87
|
throw new ValidationError("--name <name> is required", {
|
|
135
88
|
field: "name",
|
|
136
|
-
value:
|
|
89
|
+
value: opts.name,
|
|
137
90
|
});
|
|
138
91
|
}
|
|
139
92
|
|
|
@@ -150,11 +103,11 @@ async function startSupervisor(args: string[]): Promise<void> {
|
|
|
150
103
|
// Validate task exists and is workable (open or in_progress)
|
|
151
104
|
const resolvedBackend = await resolveBackend(config.taskTracker.backend, projectRoot);
|
|
152
105
|
const tracker = createTrackerClient(resolvedBackend, projectRoot);
|
|
153
|
-
const issue = await tracker.show(
|
|
106
|
+
const issue = await tracker.show(opts.task);
|
|
154
107
|
if (issue.status !== "open" && issue.status !== "in_progress") {
|
|
155
|
-
throw new ValidationError(`Task ${
|
|
108
|
+
throw new ValidationError(`Task ${opts.task} is not workable (status: ${issue.status})`, {
|
|
156
109
|
field: "task",
|
|
157
|
-
value:
|
|
110
|
+
value: opts.task,
|
|
158
111
|
});
|
|
159
112
|
}
|
|
160
113
|
|
|
@@ -162,7 +115,7 @@ async function startSupervisor(args: string[]): Promise<void> {
|
|
|
162
115
|
const overstoryDir = join(projectRoot, ".overstory");
|
|
163
116
|
const { store } = openSessionStore(overstoryDir);
|
|
164
117
|
try {
|
|
165
|
-
const existing = store.getByName(
|
|
118
|
+
const existing = store.getByName(opts.name);
|
|
166
119
|
|
|
167
120
|
if (
|
|
168
121
|
existing &&
|
|
@@ -173,24 +126,24 @@ async function startSupervisor(args: string[]): Promise<void> {
|
|
|
173
126
|
const alive = await isSessionAlive(existing.tmuxSession);
|
|
174
127
|
if (alive) {
|
|
175
128
|
throw new AgentError(
|
|
176
|
-
`Supervisor '${
|
|
177
|
-
{ agentName:
|
|
129
|
+
`Supervisor '${opts.name}' is already running (tmux: ${existing.tmuxSession}, since: ${existing.startedAt})`,
|
|
130
|
+
{ agentName: opts.name },
|
|
178
131
|
);
|
|
179
132
|
}
|
|
180
133
|
// Session recorded but tmux is dead — mark as completed and continue
|
|
181
|
-
store.updateState(
|
|
134
|
+
store.updateState(opts.name, "completed");
|
|
182
135
|
}
|
|
183
136
|
|
|
184
137
|
// Deploy supervisor-specific hooks to the project root's .claude/ directory.
|
|
185
|
-
await deployHooks(projectRoot,
|
|
138
|
+
await deployHooks(projectRoot, opts.name, "supervisor");
|
|
186
139
|
|
|
187
140
|
// Create supervisor identity if first run
|
|
188
141
|
const identityBaseDir = join(projectRoot, ".overstory", "agents");
|
|
189
142
|
await mkdir(identityBaseDir, { recursive: true });
|
|
190
|
-
const existingIdentity = await loadIdentity(identityBaseDir,
|
|
143
|
+
const existingIdentity = await loadIdentity(identityBaseDir, opts.name);
|
|
191
144
|
if (!existingIdentity) {
|
|
192
145
|
await createIdentity(identityBaseDir, {
|
|
193
|
-
name:
|
|
146
|
+
name: opts.name,
|
|
194
147
|
capability: "supervisor",
|
|
195
148
|
created: new Date().toISOString(),
|
|
196
149
|
sessionsCompleted: 0,
|
|
@@ -209,7 +162,7 @@ async function startSupervisor(args: string[]): Promise<void> {
|
|
|
209
162
|
|
|
210
163
|
// Spawn tmux session at project root with Claude Code (interactive mode).
|
|
211
164
|
// Inject the supervisor base definition via --append-system-prompt.
|
|
212
|
-
const tmuxSession = `overstory-${config.project.name}-supervisor-${
|
|
165
|
+
const tmuxSession = `overstory-${config.project.name}-supervisor-${opts.name}`;
|
|
213
166
|
const agentDefPath = join(projectRoot, ".overstory", "agent-defs", "supervisor.md");
|
|
214
167
|
const agentDefFile = Bun.file(agentDefPath);
|
|
215
168
|
let claudeCmd = `claude --model ${model} --dangerously-skip-permissions`;
|
|
@@ -220,7 +173,7 @@ async function startSupervisor(args: string[]): Promise<void> {
|
|
|
220
173
|
}
|
|
221
174
|
const pid = await createSession(tmuxSession, projectRoot, claudeCmd, {
|
|
222
175
|
...env,
|
|
223
|
-
OVERSTORY_AGENT_NAME:
|
|
176
|
+
OVERSTORY_AGENT_NAME: opts.name,
|
|
224
177
|
});
|
|
225
178
|
|
|
226
179
|
// Wait for Claude Code TUI to render before sending input
|
|
@@ -228,10 +181,10 @@ async function startSupervisor(args: string[]): Promise<void> {
|
|
|
228
181
|
await Bun.sleep(1_000);
|
|
229
182
|
|
|
230
183
|
const beacon = buildSupervisorBeacon({
|
|
231
|
-
name:
|
|
232
|
-
beadId:
|
|
233
|
-
depth:
|
|
234
|
-
parent:
|
|
184
|
+
name: opts.name,
|
|
185
|
+
beadId: opts.task,
|
|
186
|
+
depth: opts.depth,
|
|
187
|
+
parent: opts.parent,
|
|
235
188
|
trackerCli: trackerCliName(resolvedBackend),
|
|
236
189
|
});
|
|
237
190
|
await sendKeys(tmuxSession, beacon);
|
|
@@ -244,17 +197,17 @@ async function startSupervisor(args: string[]): Promise<void> {
|
|
|
244
197
|
|
|
245
198
|
// Record session
|
|
246
199
|
const session: AgentSession = {
|
|
247
|
-
id: `session-${Date.now()}-${
|
|
248
|
-
agentName:
|
|
200
|
+
id: `session-${Date.now()}-${opts.name}`,
|
|
201
|
+
agentName: opts.name,
|
|
249
202
|
capability: "supervisor",
|
|
250
203
|
worktreePath: projectRoot, // Supervisor uses project root, not a worktree
|
|
251
204
|
branchName: config.project.canonicalBranch, // Operates on canonical branch
|
|
252
|
-
beadId:
|
|
205
|
+
beadId: opts.task,
|
|
253
206
|
tmuxSession,
|
|
254
207
|
state: "booting",
|
|
255
208
|
pid,
|
|
256
|
-
parentAgent:
|
|
257
|
-
depth:
|
|
209
|
+
parentAgent: opts.parent,
|
|
210
|
+
depth: opts.depth,
|
|
258
211
|
runId: null,
|
|
259
212
|
startedAt: new Date().toISOString(),
|
|
260
213
|
lastActivity: new Date().toISOString(),
|
|
@@ -265,25 +218,25 @@ async function startSupervisor(args: string[]): Promise<void> {
|
|
|
265
218
|
store.upsert(session);
|
|
266
219
|
|
|
267
220
|
const output = {
|
|
268
|
-
agentName:
|
|
221
|
+
agentName: opts.name,
|
|
269
222
|
capability: "supervisor",
|
|
270
223
|
tmuxSession,
|
|
271
224
|
projectRoot,
|
|
272
|
-
beadId:
|
|
273
|
-
parent:
|
|
274
|
-
depth:
|
|
225
|
+
beadId: opts.task,
|
|
226
|
+
parent: opts.parent,
|
|
227
|
+
depth: opts.depth,
|
|
275
228
|
pid,
|
|
276
229
|
};
|
|
277
230
|
|
|
278
|
-
if (
|
|
231
|
+
if (opts.json) {
|
|
279
232
|
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
280
233
|
} else {
|
|
281
|
-
process.stdout.write(`Supervisor '${
|
|
234
|
+
process.stdout.write(`Supervisor '${opts.name}' started\n`);
|
|
282
235
|
process.stdout.write(` Tmux: ${tmuxSession}\n`);
|
|
283
236
|
process.stdout.write(` Root: ${projectRoot}\n`);
|
|
284
|
-
process.stdout.write(` Task: ${
|
|
285
|
-
process.stdout.write(` Parent: ${
|
|
286
|
-
process.stdout.write(` Depth: ${
|
|
237
|
+
process.stdout.write(` Task: ${opts.task}\n`);
|
|
238
|
+
process.stdout.write(` Parent: ${opts.parent}\n`);
|
|
239
|
+
process.stdout.write(` Depth: ${opts.depth}\n`);
|
|
287
240
|
process.stdout.write(` PID: ${pid}\n`);
|
|
288
241
|
}
|
|
289
242
|
} finally {
|
|
@@ -298,13 +251,11 @@ async function startSupervisor(args: string[]): Promise<void> {
|
|
|
298
251
|
* 2. Kill the tmux session (with process tree cleanup)
|
|
299
252
|
* 3. Mark session as completed in SessionStore
|
|
300
253
|
*/
|
|
301
|
-
async function stopSupervisor(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (!flags.name) {
|
|
254
|
+
async function stopSupervisor(opts: { name: string; json: boolean }): Promise<void> {
|
|
255
|
+
if (!opts.name) {
|
|
305
256
|
throw new ValidationError("--name <name> is required", {
|
|
306
257
|
field: "name",
|
|
307
|
-
value:
|
|
258
|
+
value: opts.name,
|
|
308
259
|
});
|
|
309
260
|
}
|
|
310
261
|
|
|
@@ -315,7 +266,7 @@ async function stopSupervisor(args: string[]): Promise<void> {
|
|
|
315
266
|
const overstoryDir = join(projectRoot, ".overstory");
|
|
316
267
|
const { store } = openSessionStore(overstoryDir);
|
|
317
268
|
try {
|
|
318
|
-
const session = store.getByName(
|
|
269
|
+
const session = store.getByName(opts.name);
|
|
319
270
|
|
|
320
271
|
if (
|
|
321
272
|
!session ||
|
|
@@ -323,8 +274,8 @@ async function stopSupervisor(args: string[]): Promise<void> {
|
|
|
323
274
|
session.state === "completed" ||
|
|
324
275
|
session.state === "zombie"
|
|
325
276
|
) {
|
|
326
|
-
throw new AgentError(`No active supervisor session found for '${
|
|
327
|
-
agentName:
|
|
277
|
+
throw new AgentError(`No active supervisor session found for '${opts.name}'`, {
|
|
278
|
+
agentName: opts.name,
|
|
328
279
|
});
|
|
329
280
|
}
|
|
330
281
|
|
|
@@ -335,13 +286,13 @@ async function stopSupervisor(args: string[]): Promise<void> {
|
|
|
335
286
|
}
|
|
336
287
|
|
|
337
288
|
// Update session state
|
|
338
|
-
store.updateState(
|
|
339
|
-
store.updateLastActivity(
|
|
289
|
+
store.updateState(opts.name, "completed");
|
|
290
|
+
store.updateLastActivity(opts.name);
|
|
340
291
|
|
|
341
|
-
if (
|
|
292
|
+
if (opts.json) {
|
|
342
293
|
process.stdout.write(`${JSON.stringify({ stopped: true, sessionId: session.id })}\n`);
|
|
343
294
|
} else {
|
|
344
|
-
process.stdout.write(`Supervisor '${
|
|
295
|
+
process.stdout.write(`Supervisor '${opts.name}' stopped (session: ${session.id})\n`);
|
|
345
296
|
}
|
|
346
297
|
} finally {
|
|
347
298
|
store.close();
|
|
@@ -354,8 +305,7 @@ async function stopSupervisor(args: string[]): Promise<void> {
|
|
|
354
305
|
* If --name is provided, show status for that specific supervisor.
|
|
355
306
|
* Otherwise, list all supervisors.
|
|
356
307
|
*/
|
|
357
|
-
async function statusSupervisor(
|
|
358
|
-
const flags = parseFlags(args);
|
|
308
|
+
async function statusSupervisor(opts: { name?: string; json: boolean }): Promise<void> {
|
|
359
309
|
const cwd = process.cwd();
|
|
360
310
|
const config = await loadConfig(cwd);
|
|
361
311
|
const projectRoot = config.project.root;
|
|
@@ -363,9 +313,9 @@ async function statusSupervisor(args: string[]): Promise<void> {
|
|
|
363
313
|
const overstoryDir = join(projectRoot, ".overstory");
|
|
364
314
|
const { store } = openSessionStore(overstoryDir);
|
|
365
315
|
try {
|
|
366
|
-
if (
|
|
316
|
+
if (opts.name) {
|
|
367
317
|
// Show specific supervisor
|
|
368
|
-
const session = store.getByName(
|
|
318
|
+
const session = store.getByName(opts.name);
|
|
369
319
|
|
|
370
320
|
if (
|
|
371
321
|
!session ||
|
|
@@ -373,10 +323,10 @@ async function statusSupervisor(args: string[]): Promise<void> {
|
|
|
373
323
|
session.state === "completed" ||
|
|
374
324
|
session.state === "zombie"
|
|
375
325
|
) {
|
|
376
|
-
if (
|
|
326
|
+
if (opts.json) {
|
|
377
327
|
process.stdout.write(`${JSON.stringify({ running: false })}\n`);
|
|
378
328
|
} else {
|
|
379
|
-
process.stdout.write(`Supervisor '${
|
|
329
|
+
process.stdout.write(`Supervisor '${opts.name}' is not running\n`);
|
|
380
330
|
}
|
|
381
331
|
return;
|
|
382
332
|
}
|
|
@@ -386,8 +336,8 @@ async function statusSupervisor(args: string[]): Promise<void> {
|
|
|
386
336
|
// Reconcile state: we already filtered out completed/zombie above,
|
|
387
337
|
// so if tmux is dead this session needs to be marked as zombie.
|
|
388
338
|
if (!alive) {
|
|
389
|
-
store.updateState(
|
|
390
|
-
store.updateLastActivity(
|
|
339
|
+
store.updateState(opts.name, "zombie");
|
|
340
|
+
store.updateLastActivity(opts.name);
|
|
391
341
|
session.state = "zombie";
|
|
392
342
|
}
|
|
393
343
|
|
|
@@ -405,11 +355,11 @@ async function statusSupervisor(args: string[]): Promise<void> {
|
|
|
405
355
|
lastActivity: session.lastActivity,
|
|
406
356
|
};
|
|
407
357
|
|
|
408
|
-
if (
|
|
358
|
+
if (opts.json) {
|
|
409
359
|
process.stdout.write(`${JSON.stringify(status)}\n`);
|
|
410
360
|
} else {
|
|
411
361
|
const stateLabel = alive ? "running" : session.state;
|
|
412
|
-
process.stdout.write(`Supervisor '${
|
|
362
|
+
process.stdout.write(`Supervisor '${opts.name}': ${stateLabel}\n`);
|
|
413
363
|
process.stdout.write(` Session: ${session.id}\n`);
|
|
414
364
|
process.stdout.write(` Tmux: ${session.tmuxSession}\n`);
|
|
415
365
|
process.stdout.write(` Task: ${session.beadId}\n`);
|
|
@@ -425,7 +375,7 @@ async function statusSupervisor(args: string[]): Promise<void> {
|
|
|
425
375
|
const supervisors = allSessions.filter((s) => s.capability === "supervisor");
|
|
426
376
|
|
|
427
377
|
if (supervisors.length === 0) {
|
|
428
|
-
if (
|
|
378
|
+
if (opts.json) {
|
|
429
379
|
process.stdout.write(`${JSON.stringify([])}\n`);
|
|
430
380
|
} else {
|
|
431
381
|
process.stdout.write("No supervisor sessions found\n");
|
|
@@ -459,7 +409,7 @@ async function statusSupervisor(args: string[]): Promise<void> {
|
|
|
459
409
|
}),
|
|
460
410
|
);
|
|
461
411
|
|
|
462
|
-
if (
|
|
412
|
+
if (opts.json) {
|
|
463
413
|
process.stdout.write(`${JSON.stringify(statuses)}\n`);
|
|
464
414
|
} else {
|
|
465
415
|
process.stdout.write("Supervisor sessions:\n");
|
|
@@ -476,60 +426,89 @@ async function statusSupervisor(args: string[]): Promise<void> {
|
|
|
476
426
|
}
|
|
477
427
|
}
|
|
478
428
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
429
|
+
/**
|
|
430
|
+
* Create the Commander command for `overstory supervisor`.
|
|
431
|
+
*/
|
|
432
|
+
export function createSupervisorCommand(): Command {
|
|
433
|
+
const cmd = new Command("supervisor").description("Manage per-project supervisor agents");
|
|
434
|
+
|
|
435
|
+
cmd
|
|
436
|
+
.command("start")
|
|
437
|
+
.description("Start a supervisor (spawns Claude Code at project root)")
|
|
438
|
+
.requiredOption("--task <bead-id>", "Bead task ID (required)")
|
|
439
|
+
.requiredOption("--name <name>", "Unique supervisor name (required)")
|
|
440
|
+
.option("--parent <agent>", "Parent agent name", "coordinator")
|
|
441
|
+
.option("--depth <n>", "Hierarchy depth", "1")
|
|
442
|
+
.option("--json", "Output as JSON")
|
|
443
|
+
.action(
|
|
444
|
+
async (opts: {
|
|
445
|
+
task: string;
|
|
446
|
+
name: string;
|
|
447
|
+
parent: string;
|
|
448
|
+
depth: string;
|
|
449
|
+
json?: boolean;
|
|
450
|
+
}) => {
|
|
451
|
+
await startSupervisor({
|
|
452
|
+
task: opts.task,
|
|
453
|
+
name: opts.name,
|
|
454
|
+
parent: opts.parent,
|
|
455
|
+
depth: Number.parseInt(opts.depth, 10),
|
|
456
|
+
json: opts.json ?? false,
|
|
457
|
+
});
|
|
458
|
+
},
|
|
459
|
+
);
|
|
494
460
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
461
|
+
cmd
|
|
462
|
+
.command("stop")
|
|
463
|
+
.description("Stop a supervisor (kills tmux session)")
|
|
464
|
+
.requiredOption("--name <name>", "Supervisor name to stop (required)")
|
|
465
|
+
.option("--json", "Output as JSON")
|
|
466
|
+
.action(async (opts: { name: string; json?: boolean }) => {
|
|
467
|
+
await stopSupervisor({ name: opts.name, json: opts.json ?? false });
|
|
468
|
+
});
|
|
498
469
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
470
|
+
cmd
|
|
471
|
+
.command("status")
|
|
472
|
+
.description("Show supervisor state")
|
|
473
|
+
.option("--name <name>", "Show specific supervisor (optional, lists all if omitted)")
|
|
474
|
+
.option("--json", "Output as JSON")
|
|
475
|
+
.action(async (opts: { name?: string; json?: boolean }) => {
|
|
476
|
+
await statusSupervisor({ name: opts.name, json: opts.json ?? false });
|
|
477
|
+
});
|
|
502
478
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
via overstory sling and coordinate their work.`;
|
|
479
|
+
return cmd;
|
|
480
|
+
}
|
|
506
481
|
|
|
507
482
|
/**
|
|
508
483
|
* Entry point for `overstory supervisor <subcommand>`.
|
|
509
484
|
*/
|
|
510
485
|
export async function supervisorCommand(args: string[]): Promise<void> {
|
|
511
|
-
|
|
512
|
-
|
|
486
|
+
const cmd = createSupervisorCommand();
|
|
487
|
+
cmd.exitOverride();
|
|
488
|
+
cmd.configureOutput({ writeErr: () => {} });
|
|
489
|
+
for (const sub of cmd.commands) {
|
|
490
|
+
sub.exitOverride();
|
|
491
|
+
sub.configureOutput({ writeErr: () => {} });
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (args.length === 0) {
|
|
495
|
+
process.stdout.write(cmd.helpInformation());
|
|
513
496
|
return;
|
|
514
497
|
}
|
|
515
498
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
throw new ValidationError(
|
|
531
|
-
`Unknown supervisor subcommand: ${subcommand}. Run 'overstory supervisor --help' for usage.`,
|
|
532
|
-
{ field: "subcommand", value: subcommand },
|
|
533
|
-
);
|
|
499
|
+
try {
|
|
500
|
+
await cmd.parseAsync(args, { from: "user" });
|
|
501
|
+
} catch (err: unknown) {
|
|
502
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
503
|
+
const code = (err as { code: string }).code;
|
|
504
|
+
if (code === "commander.helpDisplayed" || code === "commander.version") {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (code === "commander.unknownCommand") {
|
|
508
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
509
|
+
throw new ValidationError(message, { field: "subcommand" });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
throw err;
|
|
534
513
|
}
|
|
535
514
|
}
|
|
@@ -79,7 +79,7 @@ describe("traceCommand", () => {
|
|
|
79
79
|
await traceCommand(["--help"]);
|
|
80
80
|
const out = output();
|
|
81
81
|
|
|
82
|
-
expect(out).toContain("
|
|
82
|
+
expect(out).toContain("trace");
|
|
83
83
|
expect(out).toContain("<target>");
|
|
84
84
|
expect(out).toContain("--json");
|
|
85
85
|
expect(out).toContain("--since");
|
|
@@ -91,29 +91,29 @@ describe("traceCommand", () => {
|
|
|
91
91
|
await traceCommand(["-h"]);
|
|
92
92
|
const out = output();
|
|
93
93
|
|
|
94
|
-
expect(out).toContain("
|
|
94
|
+
expect(out).toContain("trace");
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
// === Argument parsing ===
|
|
99
99
|
|
|
100
100
|
describe("argument parsing", () => {
|
|
101
|
-
test("missing target throws
|
|
102
|
-
await expect(traceCommand([])).rejects.toThrow(
|
|
101
|
+
test("missing target throws an error", async () => {
|
|
102
|
+
await expect(traceCommand([])).rejects.toThrow();
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
test("missing target error mentions
|
|
105
|
+
test("missing target error mentions the argument name", async () => {
|
|
106
106
|
try {
|
|
107
107
|
await traceCommand([]);
|
|
108
108
|
expect.unreachable("should have thrown");
|
|
109
109
|
} catch (err) {
|
|
110
|
-
expect(err).toBeInstanceOf(
|
|
111
|
-
expect((err as
|
|
110
|
+
expect(err).toBeInstanceOf(Error);
|
|
111
|
+
expect((err as Error).message).toContain("target");
|
|
112
112
|
}
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
test("only flags with no target throws
|
|
116
|
-
await expect(traceCommand(["--json"])).rejects.toThrow(
|
|
115
|
+
test("only flags with no target throws an error", async () => {
|
|
116
|
+
await expect(traceCommand(["--json"])).rejects.toThrow();
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
test("--limit with non-numeric value throws ValidationError", async () => {
|