@os-eco/overstory-cli 0.6.7 → 0.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/agents/coordinator.md +5 -5
- package/agents/lead.md +1 -6
- package/agents/merger.md +3 -3
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +3 -3
- package/agents/supervisor.md +16 -16
- package/package.json +1 -1
- package/src/agents/hooks-deployer.test.ts +180 -0
- package/src/agents/hooks-deployer.ts +32 -1
- package/src/commands/agents.ts +9 -6
- package/src/commands/clean.ts +5 -3
- package/src/commands/completions.ts +3 -4
- package/src/commands/coordinator.test.ts +15 -12
- package/src/commands/coordinator.ts +28 -25
- package/src/commands/costs.test.ts +48 -38
- package/src/commands/costs.ts +48 -38
- package/src/commands/dashboard.ts +7 -7
- package/src/commands/doctor.test.ts +8 -0
- package/src/commands/doctor.ts +2 -6
- package/src/commands/errors.test.ts +47 -40
- package/src/commands/errors.ts +5 -4
- package/src/commands/feed.test.ts +40 -33
- package/src/commands/feed.ts +3 -2
- package/src/commands/group.ts +28 -18
- package/src/commands/hooks.test.ts +1 -1
- package/src/commands/hooks.ts +9 -9
- package/src/commands/init.test.ts +105 -5
- package/src/commands/init.ts +17 -12
- package/src/commands/inspect.test.ts +2 -0
- package/src/commands/inspect.ts +9 -8
- package/src/commands/logs.test.ts +5 -6
- package/src/commands/logs.ts +2 -1
- package/src/commands/mail.test.ts +17 -16
- package/src/commands/mail.ts +17 -17
- package/src/commands/merge.ts +12 -12
- package/src/commands/metrics.test.ts +15 -2
- package/src/commands/metrics.ts +3 -2
- package/src/commands/monitor.ts +9 -7
- package/src/commands/nudge.ts +4 -4
- package/src/commands/prime.test.ts +1 -6
- package/src/commands/prime.ts +2 -3
- package/src/commands/replay.test.ts +62 -55
- package/src/commands/replay.ts +3 -2
- package/src/commands/run.ts +24 -26
- package/src/commands/sling.ts +4 -2
- package/src/commands/spec.test.ts +10 -8
- package/src/commands/spec.ts +3 -2
- package/src/commands/status.test.ts +2 -1
- package/src/commands/status.ts +7 -6
- package/src/commands/stop.test.ts +10 -6
- package/src/commands/stop.ts +13 -13
- package/src/commands/supervisor.ts +12 -10
- package/src/commands/trace.test.ts +52 -44
- package/src/commands/trace.ts +5 -4
- package/src/commands/watch.ts +8 -10
- package/src/commands/worktree.test.ts +27 -20
- package/src/commands/worktree.ts +29 -30
- package/src/doctor/version.ts +2 -2
- package/src/e2e/init-sling-lifecycle.test.ts +1 -5
- package/src/index.ts +99 -14
- package/src/json.test.ts +72 -0
- package/src/json.ts +24 -0
- package/src/logging/color.test.ts +127 -0
- package/src/logging/color.ts +28 -0
- package/src/mulch/client.test.ts +22 -22
- package/src/worktree/tmux.test.ts +123 -5
- package/src/worktree/tmux.ts +38 -8
- package/agents/issue-reviews.md +0 -71
- package/agents/pr-reviews.md +0 -60
- package/agents/prioritize.md +0 -110
- package/agents/release.md +0 -56
package/src/commands/metrics.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
import { loadConfig } from "../config.ts";
|
|
11
|
+
import { jsonOutput } from "../json.ts";
|
|
11
12
|
import { createMetricsStore } from "../metrics/store.ts";
|
|
12
13
|
|
|
13
14
|
interface MetricsOpts {
|
|
@@ -41,7 +42,7 @@ async function executeMetrics(opts: MetricsOpts): Promise<void> {
|
|
|
41
42
|
const dbFile = Bun.file(dbPath);
|
|
42
43
|
if (!(await dbFile.exists())) {
|
|
43
44
|
if (json) {
|
|
44
|
-
|
|
45
|
+
jsonOutput("metrics", { sessions: [] });
|
|
45
46
|
} else {
|
|
46
47
|
process.stdout.write("No metrics data yet.\n");
|
|
47
48
|
}
|
|
@@ -54,7 +55,7 @@ async function executeMetrics(opts: MetricsOpts): Promise<void> {
|
|
|
54
55
|
const sessions = store.getRecentSessions(limit);
|
|
55
56
|
|
|
56
57
|
if (json) {
|
|
57
|
-
|
|
58
|
+
jsonOutput("metrics", { sessions } as Record<string, unknown>);
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
60
61
|
|
package/src/commands/monitor.ts
CHANGED
|
@@ -21,6 +21,8 @@ import { createIdentity, loadIdentity } from "../agents/identity.ts";
|
|
|
21
21
|
import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
|
|
22
22
|
import { loadConfig } from "../config.ts";
|
|
23
23
|
import { AgentError, ValidationError } from "../errors.ts";
|
|
24
|
+
import { jsonOutput } from "../json.ts";
|
|
25
|
+
import { printHint, printSuccess } from "../logging/color.ts";
|
|
24
26
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
25
27
|
import type { AgentSession } from "../types.ts";
|
|
26
28
|
import { createSession, isSessionAlive, killSession, sendKeys } from "../worktree/tmux.ts";
|
|
@@ -189,9 +191,9 @@ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<v
|
|
|
189
191
|
};
|
|
190
192
|
|
|
191
193
|
if (json) {
|
|
192
|
-
|
|
194
|
+
jsonOutput("monitor start", output);
|
|
193
195
|
} else {
|
|
194
|
-
|
|
196
|
+
printSuccess("Monitor started");
|
|
195
197
|
process.stdout.write(` Tmux: ${tmuxSession}\n`);
|
|
196
198
|
process.stdout.write(` Root: ${projectRoot}\n`);
|
|
197
199
|
process.stdout.write(` PID: ${pid}\n`);
|
|
@@ -247,9 +249,9 @@ async function stopMonitor(opts: { json: boolean }): Promise<void> {
|
|
|
247
249
|
store.updateLastActivity(MONITOR_NAME);
|
|
248
250
|
|
|
249
251
|
if (json) {
|
|
250
|
-
|
|
252
|
+
jsonOutput("monitor stop", { stopped: true, sessionId: session.id });
|
|
251
253
|
} else {
|
|
252
|
-
|
|
254
|
+
printSuccess("Monitor stopped", session.id);
|
|
253
255
|
}
|
|
254
256
|
} finally {
|
|
255
257
|
store.close();
|
|
@@ -279,9 +281,9 @@ async function statusMonitor(opts: { json: boolean }): Promise<void> {
|
|
|
279
281
|
session.state === "zombie"
|
|
280
282
|
) {
|
|
281
283
|
if (json) {
|
|
282
|
-
|
|
284
|
+
jsonOutput("monitor status", { running: false });
|
|
283
285
|
} else {
|
|
284
|
-
|
|
286
|
+
printHint("Monitor is not running");
|
|
285
287
|
}
|
|
286
288
|
return;
|
|
287
289
|
}
|
|
@@ -306,7 +308,7 @@ async function statusMonitor(opts: { json: boolean }): Promise<void> {
|
|
|
306
308
|
};
|
|
307
309
|
|
|
308
310
|
if (json) {
|
|
309
|
-
|
|
311
|
+
jsonOutput("monitor status", status);
|
|
310
312
|
} else {
|
|
311
313
|
const stateLabel = alive ? "running" : session.state;
|
|
312
314
|
process.stdout.write(`Monitor: ${stateLabel}\n`);
|
package/src/commands/nudge.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { join } from "node:path";
|
|
|
13
13
|
import { Command } from "commander";
|
|
14
14
|
import { AgentError } from "../errors.ts";
|
|
15
15
|
import { createEventStore } from "../events/store.ts";
|
|
16
|
+
import { jsonOutput } from "../json.ts";
|
|
17
|
+
import { printSuccess } from "../logging/color.ts";
|
|
16
18
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
17
19
|
import type { EventStore } from "../types.ts";
|
|
18
20
|
import { isSessionAlive, sendKeys } from "../worktree/tmux.ts";
|
|
@@ -309,11 +311,9 @@ export async function nudgeCommand(args: string[]): Promise<void> {
|
|
|
309
311
|
const result = await nudgeAgent(projectRoot, agentName, message, opts.force ?? false);
|
|
310
312
|
|
|
311
313
|
if (opts.json) {
|
|
312
|
-
|
|
313
|
-
`${JSON.stringify({ agentName, delivered: result.delivered, reason: result.reason })}\n`,
|
|
314
|
-
);
|
|
314
|
+
jsonOutput("nudge", { agentName, delivered: result.delivered, reason: result.reason });
|
|
315
315
|
} else if (result.delivered) {
|
|
316
|
-
|
|
316
|
+
printSuccess("Nudge delivered", agentName);
|
|
317
317
|
} else {
|
|
318
318
|
throw new AgentError(`Nudge failed: ${result.reason}`, { agentName });
|
|
319
319
|
}
|
|
@@ -62,10 +62,6 @@ describe("primeCommand", () => {
|
|
|
62
62
|
return chunks.join("");
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function stderr(): string {
|
|
66
|
-
return stderrChunks.join("");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
65
|
describe("Orchestrator priming (no --agent flag)", () => {
|
|
70
66
|
test("default prime outputs project context", async () => {
|
|
71
67
|
await primeCommand({});
|
|
@@ -113,12 +109,11 @@ describe("primeCommand", () => {
|
|
|
113
109
|
test("unknown agent outputs basic context and warns", async () => {
|
|
114
110
|
await primeCommand({ agent: "unknown-agent" });
|
|
115
111
|
const out = output();
|
|
116
|
-
const err = stderr();
|
|
117
112
|
|
|
118
113
|
expect(out).toContain("# Agent Context: unknown-agent");
|
|
119
114
|
expect(out).toContain("## Identity");
|
|
120
115
|
expect(out).toContain("New agent - no prior sessions");
|
|
121
|
-
expect(
|
|
116
|
+
expect(out).toContain('agent "unknown-agent" not found');
|
|
122
117
|
});
|
|
123
118
|
|
|
124
119
|
test("agent with identity.yaml shows identity details", async () => {
|
package/src/commands/prime.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { loadCheckpoint } from "../agents/checkpoint.ts";
|
|
|
12
12
|
import { loadIdentity } from "../agents/identity.ts";
|
|
13
13
|
import { createManifestLoader } from "../agents/manifest.ts";
|
|
14
14
|
import { loadConfig } from "../config.ts";
|
|
15
|
+
import { printWarning } from "../logging/color.ts";
|
|
15
16
|
import { createMetricsStore } from "../metrics/store.ts";
|
|
16
17
|
import { createMulchClient } from "../mulch/client.ts";
|
|
17
18
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
@@ -211,9 +212,7 @@ async function outputAgentContext(
|
|
|
211
212
|
|
|
212
213
|
// Warn if agent is completely unknown (no session and no identity)
|
|
213
214
|
if (!sessionExists && identity === null) {
|
|
214
|
-
|
|
215
|
-
`Warning: agent "${agentName}" not found in sessions or identity store.\n`,
|
|
216
|
-
);
|
|
215
|
+
printWarning(`agent "${agentName}" not found in sessions or identity store.`);
|
|
217
216
|
}
|
|
218
217
|
|
|
219
218
|
sections.push("\n## Identity");
|
|
@@ -133,7 +133,14 @@ describe("replayCommand", () => {
|
|
|
133
133
|
await replayCommand(["--json"]);
|
|
134
134
|
const out = output();
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
137
|
+
success: boolean;
|
|
138
|
+
command: string;
|
|
139
|
+
events: unknown[];
|
|
140
|
+
};
|
|
141
|
+
expect(parsed.success).toBe(true);
|
|
142
|
+
expect(parsed.command).toBe("replay");
|
|
143
|
+
expect(parsed.events).toEqual([]);
|
|
137
144
|
});
|
|
138
145
|
});
|
|
139
146
|
|
|
@@ -151,9 +158,9 @@ describe("replayCommand", () => {
|
|
|
151
158
|
await replayCommand(["--run", "run-001", "--json"]);
|
|
152
159
|
const out = output();
|
|
153
160
|
|
|
154
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
155
|
-
expect(parsed).toHaveLength(3);
|
|
156
|
-
expect(Array.isArray(parsed)).toBe(true);
|
|
161
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
162
|
+
expect(parsed.events).toHaveLength(3);
|
|
163
|
+
expect(Array.isArray(parsed.events)).toBe(true);
|
|
157
164
|
});
|
|
158
165
|
|
|
159
166
|
test("JSON output includes expected fields", async () => {
|
|
@@ -172,9 +179,9 @@ describe("replayCommand", () => {
|
|
|
172
179
|
await replayCommand(["--run", "run-001", "--json"]);
|
|
173
180
|
const out = output();
|
|
174
181
|
|
|
175
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
176
|
-
expect(parsed).toHaveLength(1);
|
|
177
|
-
const event = parsed[0];
|
|
182
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
183
|
+
expect(parsed.events).toHaveLength(1);
|
|
184
|
+
const event = parsed.events[0];
|
|
178
185
|
expect(event).toBeDefined();
|
|
179
186
|
expect(event?.agentName).toBe("builder-1");
|
|
180
187
|
expect(event?.eventType).toBe("tool_start");
|
|
@@ -192,8 +199,8 @@ describe("replayCommand", () => {
|
|
|
192
199
|
await replayCommand(["--run", "run-001", "--json"]);
|
|
193
200
|
const out = output();
|
|
194
201
|
|
|
195
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
196
|
-
expect(parsed).toEqual([]);
|
|
202
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
203
|
+
expect(parsed.events).toEqual([]);
|
|
197
204
|
});
|
|
198
205
|
});
|
|
199
206
|
|
|
@@ -332,9 +339,9 @@ describe("replayCommand", () => {
|
|
|
332
339
|
await replayCommand(["--run", "run-001", "--json"]);
|
|
333
340
|
const out = output();
|
|
334
341
|
|
|
335
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
336
|
-
expect(parsed).toHaveLength(2);
|
|
337
|
-
for (const event of parsed) {
|
|
342
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
343
|
+
expect(parsed.events).toHaveLength(2);
|
|
344
|
+
for (const event of parsed.events) {
|
|
338
345
|
expect(event.runId).toBe("run-001");
|
|
339
346
|
}
|
|
340
347
|
});
|
|
@@ -349,8 +356,8 @@ describe("replayCommand", () => {
|
|
|
349
356
|
await replayCommand(["--run", "run-001", "--json", "--since", "2099-01-01T00:00:00Z"]);
|
|
350
357
|
const out = output();
|
|
351
358
|
|
|
352
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
353
|
-
expect(parsed).toEqual([]);
|
|
359
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
360
|
+
expect(parsed.events).toEqual([]);
|
|
354
361
|
});
|
|
355
362
|
});
|
|
356
363
|
|
|
@@ -368,9 +375,9 @@ describe("replayCommand", () => {
|
|
|
368
375
|
await replayCommand(["--agent", "builder-1", "--json"]);
|
|
369
376
|
const out = output();
|
|
370
377
|
|
|
371
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
372
|
-
expect(parsed).toHaveLength(2);
|
|
373
|
-
for (const event of parsed) {
|
|
378
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
379
|
+
expect(parsed.events).toHaveLength(2);
|
|
380
|
+
for (const event of parsed.events) {
|
|
374
381
|
expect(event.agentName).toBe("builder-1");
|
|
375
382
|
}
|
|
376
383
|
});
|
|
@@ -387,9 +394,9 @@ describe("replayCommand", () => {
|
|
|
387
394
|
await replayCommand(["--agent", "builder-1", "--agent", "scout-1", "--json"]);
|
|
388
395
|
const out = output();
|
|
389
396
|
|
|
390
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
391
|
-
expect(parsed).toHaveLength(3);
|
|
392
|
-
const agents = new Set(parsed.map((e) => e.agentName));
|
|
397
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
398
|
+
expect(parsed.events).toHaveLength(3);
|
|
399
|
+
const agents = new Set(parsed.events.map((e) => e.agentName));
|
|
393
400
|
expect(agents.has("builder-1")).toBe(true);
|
|
394
401
|
expect(agents.has("scout-1")).toBe(true);
|
|
395
402
|
expect(agents.has("reviewer-1")).toBe(false);
|
|
@@ -407,12 +414,12 @@ describe("replayCommand", () => {
|
|
|
407
414
|
await replayCommand(["--agent", "builder-1", "--agent", "scout-1", "--json"]);
|
|
408
415
|
const out = output();
|
|
409
416
|
|
|
410
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
411
|
-
expect(parsed).toHaveLength(3);
|
|
417
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
418
|
+
expect(parsed.events).toHaveLength(3);
|
|
412
419
|
// They should be in chronological order
|
|
413
|
-
for (let i = 1; i < parsed.length; i++) {
|
|
414
|
-
const prev = parsed[i - 1];
|
|
415
|
-
const curr = parsed[i];
|
|
420
|
+
for (let i = 1; i < parsed.events.length; i++) {
|
|
421
|
+
const prev = parsed.events[i - 1];
|
|
422
|
+
const curr = parsed.events[i];
|
|
416
423
|
if (prev && curr) {
|
|
417
424
|
expect(
|
|
418
425
|
(prev.createdAt as string).localeCompare(curr.createdAt as string),
|
|
@@ -430,8 +437,8 @@ describe("replayCommand", () => {
|
|
|
430
437
|
await replayCommand(["--agent", "nonexistent", "--json"]);
|
|
431
438
|
const out = output();
|
|
432
439
|
|
|
433
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
434
|
-
expect(parsed).toEqual([]);
|
|
440
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
441
|
+
expect(parsed.events).toEqual([]);
|
|
435
442
|
});
|
|
436
443
|
});
|
|
437
444
|
|
|
@@ -451,9 +458,9 @@ describe("replayCommand", () => {
|
|
|
451
458
|
await replayCommand(["--json"]);
|
|
452
459
|
const out = output();
|
|
453
460
|
|
|
454
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
455
|
-
expect(parsed).toHaveLength(1);
|
|
456
|
-
expect(parsed[0]?.runId).toBe("run-from-file");
|
|
461
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
462
|
+
expect(parsed.events).toHaveLength(1);
|
|
463
|
+
expect(parsed.events[0]?.runId).toBe("run-from-file");
|
|
457
464
|
});
|
|
458
465
|
|
|
459
466
|
test("falls back to 24h timeline when no current-run.txt", async () => {
|
|
@@ -467,8 +474,8 @@ describe("replayCommand", () => {
|
|
|
467
474
|
await replayCommand(["--json"]);
|
|
468
475
|
const out = output();
|
|
469
476
|
|
|
470
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
471
|
-
expect(parsed).toHaveLength(2);
|
|
477
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
478
|
+
expect(parsed.events).toHaveLength(2);
|
|
472
479
|
});
|
|
473
480
|
|
|
474
481
|
test("falls back to timeline when current-run.txt is empty", async () => {
|
|
@@ -482,8 +489,8 @@ describe("replayCommand", () => {
|
|
|
482
489
|
await replayCommand(["--json"]);
|
|
483
490
|
const out = output();
|
|
484
491
|
|
|
485
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
486
|
-
expect(parsed).toHaveLength(1);
|
|
492
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
493
|
+
expect(parsed.events).toHaveLength(1);
|
|
487
494
|
});
|
|
488
495
|
});
|
|
489
496
|
|
|
@@ -501,8 +508,8 @@ describe("replayCommand", () => {
|
|
|
501
508
|
await replayCommand(["--run", "run-001", "--json", "--limit", "3"]);
|
|
502
509
|
const out = output();
|
|
503
510
|
|
|
504
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
505
|
-
expect(parsed).toHaveLength(3);
|
|
511
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
512
|
+
expect(parsed.events).toHaveLength(3);
|
|
506
513
|
});
|
|
507
514
|
|
|
508
515
|
test("default limit is 200", async () => {
|
|
@@ -516,8 +523,8 @@ describe("replayCommand", () => {
|
|
|
516
523
|
await replayCommand(["--run", "run-001", "--json"]);
|
|
517
524
|
const out = output();
|
|
518
525
|
|
|
519
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
520
|
-
expect(parsed).toHaveLength(200);
|
|
526
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
527
|
+
expect(parsed.events).toHaveLength(200);
|
|
521
528
|
});
|
|
522
529
|
|
|
523
530
|
test("--limit applies to merged --agent queries", async () => {
|
|
@@ -532,8 +539,8 @@ describe("replayCommand", () => {
|
|
|
532
539
|
await replayCommand(["--agent", "builder-1", "--agent", "scout-1", "--json", "--limit", "4"]);
|
|
533
540
|
const out = output();
|
|
534
541
|
|
|
535
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
536
|
-
expect(parsed).toHaveLength(4);
|
|
542
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
543
|
+
expect(parsed.events).toHaveLength(4);
|
|
537
544
|
});
|
|
538
545
|
});
|
|
539
546
|
|
|
@@ -550,8 +557,8 @@ describe("replayCommand", () => {
|
|
|
550
557
|
await replayCommand(["--run", "run-001", "--json", "--since", "2099-01-01T00:00:00Z"]);
|
|
551
558
|
const out = output();
|
|
552
559
|
|
|
553
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
554
|
-
expect(parsed).toEqual([]);
|
|
560
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
561
|
+
expect(parsed.events).toEqual([]);
|
|
555
562
|
});
|
|
556
563
|
|
|
557
564
|
test("--since with past timestamp returns all events", async () => {
|
|
@@ -564,8 +571,8 @@ describe("replayCommand", () => {
|
|
|
564
571
|
await replayCommand(["--run", "run-001", "--json", "--since", "2020-01-01T00:00:00Z"]);
|
|
565
572
|
const out = output();
|
|
566
573
|
|
|
567
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
568
|
-
expect(parsed).toHaveLength(2);
|
|
574
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
575
|
+
expect(parsed.events).toHaveLength(2);
|
|
569
576
|
});
|
|
570
577
|
|
|
571
578
|
test("--until with past timestamp returns no events", async () => {
|
|
@@ -577,8 +584,8 @@ describe("replayCommand", () => {
|
|
|
577
584
|
await replayCommand(["--run", "run-001", "--json", "--until", "2000-01-01T00:00:00Z"]);
|
|
578
585
|
const out = output();
|
|
579
586
|
|
|
580
|
-
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
581
|
-
expect(parsed).toEqual([]);
|
|
587
|
+
const parsed = JSON.parse(out.trim()) as { events: unknown[] };
|
|
588
|
+
expect(parsed.events).toEqual([]);
|
|
582
589
|
});
|
|
583
590
|
|
|
584
591
|
test("--since causes absolute timestamps in text mode", async () => {
|
|
@@ -611,12 +618,12 @@ describe("replayCommand", () => {
|
|
|
611
618
|
await replayCommand(["--run", "run-001", "--json"]);
|
|
612
619
|
const out = output();
|
|
613
620
|
|
|
614
|
-
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
615
|
-
expect(parsed).toHaveLength(4);
|
|
621
|
+
const parsed = JSON.parse(out.trim()) as { events: Record<string, unknown>[] };
|
|
622
|
+
expect(parsed.events).toHaveLength(4);
|
|
616
623
|
// Verify chronological order
|
|
617
|
-
for (let i = 1; i < parsed.length; i++) {
|
|
618
|
-
const prev = parsed[i - 1];
|
|
619
|
-
const curr = parsed[i];
|
|
624
|
+
for (let i = 1; i < parsed.events.length; i++) {
|
|
625
|
+
const prev = parsed.events[i - 1];
|
|
626
|
+
const curr = parsed.events[i];
|
|
620
627
|
if (prev && curr) {
|
|
621
628
|
expect(
|
|
622
629
|
(prev.createdAt as string).localeCompare(curr.createdAt as string),
|
|
@@ -624,10 +631,10 @@ describe("replayCommand", () => {
|
|
|
624
631
|
}
|
|
625
632
|
}
|
|
626
633
|
// Verify interleaving: agents should alternate
|
|
627
|
-
expect(parsed[0]?.agentName).toBe("builder-1");
|
|
628
|
-
expect(parsed[1]?.agentName).toBe("scout-1");
|
|
629
|
-
expect(parsed[2]?.agentName).toBe("builder-1");
|
|
630
|
-
expect(parsed[3]?.agentName).toBe("scout-1");
|
|
634
|
+
expect(parsed.events[0]?.agentName).toBe("builder-1");
|
|
635
|
+
expect(parsed.events[1]?.agentName).toBe("scout-1");
|
|
636
|
+
expect(parsed.events[2]?.agentName).toBe("builder-1");
|
|
637
|
+
expect(parsed.events[3]?.agentName).toBe("scout-1");
|
|
631
638
|
});
|
|
632
639
|
});
|
|
633
640
|
|
package/src/commands/replay.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { Command } from "commander";
|
|
|
11
11
|
import { loadConfig } from "../config.ts";
|
|
12
12
|
import { ValidationError } from "../errors.ts";
|
|
13
13
|
import { createEventStore } from "../events/store.ts";
|
|
14
|
+
import { jsonOutput } from "../json.ts";
|
|
14
15
|
import type { ColorFn } from "../logging/color.ts";
|
|
15
16
|
import { color } from "../logging/color.ts";
|
|
16
17
|
import type { EventType, StoredEvent } from "../types.ts";
|
|
@@ -246,7 +247,7 @@ async function executeReplay(opts: ReplayOpts): Promise<void> {
|
|
|
246
247
|
const eventsFile = Bun.file(eventsDbPath);
|
|
247
248
|
if (!(await eventsFile.exists())) {
|
|
248
249
|
if (json) {
|
|
249
|
-
|
|
250
|
+
jsonOutput("replay", { events: [] });
|
|
250
251
|
} else {
|
|
251
252
|
process.stdout.write("No events data yet.\n");
|
|
252
253
|
}
|
|
@@ -305,7 +306,7 @@ async function executeReplay(opts: ReplayOpts): Promise<void> {
|
|
|
305
306
|
}
|
|
306
307
|
|
|
307
308
|
if (json) {
|
|
308
|
-
|
|
309
|
+
jsonOutput("replay", { events });
|
|
309
310
|
return;
|
|
310
311
|
}
|
|
311
312
|
|
package/src/commands/run.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { join } from "node:path";
|
|
|
15
15
|
import { Command, CommanderError } from "commander";
|
|
16
16
|
import { loadConfig } from "../config.ts";
|
|
17
17
|
import { ValidationError } from "../errors.ts";
|
|
18
|
+
import { jsonError, jsonOutput } from "../json.ts";
|
|
19
|
+
import { accent, printError, printHint, printSuccess } from "../logging/color.ts";
|
|
18
20
|
import { createRunStore, createSessionStore } from "../sessions/store.ts";
|
|
19
21
|
import type { AgentSession, Run } from "../types.ts";
|
|
20
22
|
|
|
@@ -82,9 +84,9 @@ async function showCurrentRun(overstoryDir: string, json: boolean): Promise<void
|
|
|
82
84
|
const runId = await readCurrentRunId(overstoryDir);
|
|
83
85
|
if (!runId) {
|
|
84
86
|
if (json) {
|
|
85
|
-
|
|
87
|
+
jsonOutput("run", { run: null, message: "No active run" });
|
|
86
88
|
} else {
|
|
87
|
-
|
|
89
|
+
printHint("No active run");
|
|
88
90
|
}
|
|
89
91
|
return;
|
|
90
92
|
}
|
|
@@ -95,23 +97,21 @@ async function showCurrentRun(overstoryDir: string, json: boolean): Promise<void
|
|
|
95
97
|
const run = runStore.getRun(runId);
|
|
96
98
|
if (!run) {
|
|
97
99
|
if (json) {
|
|
98
|
-
|
|
99
|
-
`${JSON.stringify({ run: null, message: `Run ${runId} not found in store` })}\n`,
|
|
100
|
-
);
|
|
100
|
+
jsonOutput("run", { run: null, message: `Run ${runId} not found in store` });
|
|
101
101
|
} else {
|
|
102
|
-
process.stdout.write(`Run ${runId} not found in store\n`);
|
|
102
|
+
process.stdout.write(`Run ${accent(runId)} not found in store\n`);
|
|
103
103
|
}
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
if (json) {
|
|
108
|
-
|
|
108
|
+
jsonOutput("run", { run, duration: runDuration(run) });
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
process.stdout.write("Current Run\n");
|
|
113
113
|
process.stdout.write(`${"=".repeat(50)}\n`);
|
|
114
|
-
process.stdout.write(` ID: ${run.id}\n`);
|
|
114
|
+
process.stdout.write(` ID: ${accent(run.id)}\n`);
|
|
115
115
|
process.stdout.write(` Status: ${run.status}\n`);
|
|
116
116
|
process.stdout.write(` Started: ${run.startedAt}\n`);
|
|
117
117
|
process.stdout.write(` Agents: ${run.agentCount}\n`);
|
|
@@ -129,9 +129,9 @@ async function listRuns(overstoryDir: string, limit: number, json: boolean): Pro
|
|
|
129
129
|
const dbFile = Bun.file(dbPath);
|
|
130
130
|
if (!(await dbFile.exists())) {
|
|
131
131
|
if (json) {
|
|
132
|
-
|
|
132
|
+
jsonOutput("run list", { runs: [] });
|
|
133
133
|
} else {
|
|
134
|
-
|
|
134
|
+
printHint("No runs recorded yet");
|
|
135
135
|
}
|
|
136
136
|
return;
|
|
137
137
|
}
|
|
@@ -142,12 +142,12 @@ async function listRuns(overstoryDir: string, limit: number, json: boolean): Pro
|
|
|
142
142
|
|
|
143
143
|
if (json) {
|
|
144
144
|
const runsWithDuration = runs.map((r) => ({ ...r, duration: runDuration(r) }));
|
|
145
|
-
|
|
145
|
+
jsonOutput("run list", { runs: runsWithDuration });
|
|
146
146
|
return;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
if (runs.length === 0) {
|
|
150
|
-
|
|
150
|
+
printHint("No runs recorded yet");
|
|
151
151
|
return;
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -159,7 +159,7 @@ async function listRuns(overstoryDir: string, limit: number, json: boolean): Pro
|
|
|
159
159
|
process.stdout.write(`${"-".repeat(70)}\n`);
|
|
160
160
|
|
|
161
161
|
for (const run of runs) {
|
|
162
|
-
const id = run.id.length > 35 ? `${run.id.slice(0, 32)}...` : run.id.padEnd(36);
|
|
162
|
+
const id = accent(run.id.length > 35 ? `${run.id.slice(0, 32)}...` : run.id.padEnd(36));
|
|
163
163
|
const status = run.status.padEnd(10);
|
|
164
164
|
const agents = String(run.agentCount).padEnd(7);
|
|
165
165
|
const duration = runDuration(run);
|
|
@@ -177,9 +177,9 @@ async function completeCurrentRun(overstoryDir: string, json: boolean): Promise<
|
|
|
177
177
|
const runId = await readCurrentRunId(overstoryDir);
|
|
178
178
|
if (!runId) {
|
|
179
179
|
if (json) {
|
|
180
|
-
|
|
180
|
+
jsonError("run complete", "No active run to complete");
|
|
181
181
|
} else {
|
|
182
|
-
|
|
182
|
+
printError("No active run to complete");
|
|
183
183
|
}
|
|
184
184
|
process.exitCode = 1;
|
|
185
185
|
return;
|
|
@@ -202,9 +202,9 @@ async function completeCurrentRun(overstoryDir: string, json: boolean): Promise<
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
if (json) {
|
|
205
|
-
|
|
205
|
+
jsonOutput("run complete", { runId, status: "completed" });
|
|
206
206
|
} else {
|
|
207
|
-
|
|
207
|
+
printSuccess("Run completed", runId);
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
|
|
@@ -216,9 +216,9 @@ async function showRun(overstoryDir: string, runId: string, json: boolean): Prom
|
|
|
216
216
|
const dbFile = Bun.file(dbPath);
|
|
217
217
|
if (!(await dbFile.exists())) {
|
|
218
218
|
if (json) {
|
|
219
|
-
|
|
219
|
+
jsonError("run show", `Run ${runId} not found`);
|
|
220
220
|
} else {
|
|
221
|
-
|
|
221
|
+
printError("Run not found", runId);
|
|
222
222
|
}
|
|
223
223
|
process.exitCode = 1;
|
|
224
224
|
return;
|
|
@@ -230,11 +230,9 @@ async function showRun(overstoryDir: string, runId: string, json: boolean): Prom
|
|
|
230
230
|
const run = runStore.getRun(runId);
|
|
231
231
|
if (!run) {
|
|
232
232
|
if (json) {
|
|
233
|
-
|
|
234
|
-
`${JSON.stringify({ run: null, message: `Run ${runId} not found` })}\n`,
|
|
235
|
-
);
|
|
233
|
+
jsonError("run show", `Run ${runId} not found`);
|
|
236
234
|
} else {
|
|
237
|
-
|
|
235
|
+
printError("Run not found", runId);
|
|
238
236
|
}
|
|
239
237
|
process.exitCode = 1;
|
|
240
238
|
return;
|
|
@@ -243,13 +241,13 @@ async function showRun(overstoryDir: string, runId: string, json: boolean): Prom
|
|
|
243
241
|
const agents = sessionStore.getByRun(runId);
|
|
244
242
|
|
|
245
243
|
if (json) {
|
|
246
|
-
|
|
244
|
+
jsonOutput("run show", { run, duration: runDuration(run), agents });
|
|
247
245
|
return;
|
|
248
246
|
}
|
|
249
247
|
|
|
250
248
|
process.stdout.write("Run Details\n");
|
|
251
249
|
process.stdout.write(`${"=".repeat(60)}\n`);
|
|
252
|
-
process.stdout.write(` ID: ${run.id}\n`);
|
|
250
|
+
process.stdout.write(` ID: ${accent(run.id)}\n`);
|
|
253
251
|
process.stdout.write(` Status: ${run.status}\n`);
|
|
254
252
|
process.stdout.write(` Started: ${run.startedAt}\n`);
|
|
255
253
|
if (run.completedAt) {
|
|
@@ -264,7 +262,7 @@ async function showRun(overstoryDir: string, runId: string, json: boolean): Prom
|
|
|
264
262
|
for (const agent of agents) {
|
|
265
263
|
const agentDuration = formatAgentDuration(agent);
|
|
266
264
|
process.stdout.write(
|
|
267
|
-
` ${agent.agentName} [${agent.capability}] ${agent.state} | ${agentDuration}\n`,
|
|
265
|
+
` ${accent(agent.agentName)} [${agent.capability}] ${agent.state} | ${agentDuration}\n`,
|
|
268
266
|
);
|
|
269
267
|
}
|
|
270
268
|
} else {
|
package/src/commands/sling.ts
CHANGED
|
@@ -27,6 +27,8 @@ import { writeOverlay } from "../agents/overlay.ts";
|
|
|
27
27
|
import { loadConfig } from "../config.ts";
|
|
28
28
|
import { AgentError, HierarchyError, ValidationError } from "../errors.ts";
|
|
29
29
|
import { inferDomain } from "../insights/analyzer.ts";
|
|
30
|
+
import { jsonOutput } from "../json.ts";
|
|
31
|
+
import { printSuccess } from "../logging/color.ts";
|
|
30
32
|
import { createMailClient } from "../mail/client.ts";
|
|
31
33
|
import { createMailStore } from "../mail/store.ts";
|
|
32
34
|
import { createMulchClient } from "../mulch/client.ts";
|
|
@@ -707,9 +709,9 @@ export async function slingCommand(taskId: string, opts: SlingOptions): Promise<
|
|
|
707
709
|
};
|
|
708
710
|
|
|
709
711
|
if (opts.json ?? false) {
|
|
710
|
-
|
|
712
|
+
jsonOutput("sling", output);
|
|
711
713
|
} else {
|
|
712
|
-
|
|
714
|
+
printSuccess("Agent launched", name);
|
|
713
715
|
process.stdout.write(` Task: ${taskId}\n`);
|
|
714
716
|
process.stdout.write(` Branch: ${branchName}\n`);
|
|
715
717
|
process.stdout.write(` Worktree: ${worktreePath}\n`);
|
|
@@ -142,13 +142,13 @@ describe("writeSpec", () => {
|
|
|
142
142
|
// === specWriteCommand (CLI integration) ===
|
|
143
143
|
|
|
144
144
|
describe("specWriteCommand (integration)", () => {
|
|
145
|
-
test("writes spec and prints
|
|
145
|
+
test("writes spec and prints success", async () => {
|
|
146
146
|
await specWriteCommand("task-cmd", { body: "# CLI Spec" });
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
expect(stdoutOutput
|
|
148
|
+
expect(stdoutOutput).toContain("Spec written");
|
|
149
|
+
expect(stdoutOutput).toContain("task-cmd");
|
|
150
150
|
|
|
151
|
-
const specPath =
|
|
151
|
+
const specPath = join(tempDir, ".overstory", "specs", "task-cmd.md");
|
|
152
152
|
const content = await Bun.file(specPath).text();
|
|
153
153
|
expect(content).toBe("# CLI Spec\n");
|
|
154
154
|
});
|
|
@@ -156,9 +156,10 @@ describe("specWriteCommand (integration)", () => {
|
|
|
156
156
|
test("writes spec with agent attribution", async () => {
|
|
157
157
|
await specWriteCommand("task-attr", { body: "# Attributed", agent: "scout-2" });
|
|
158
158
|
|
|
159
|
-
expect(stdoutOutput
|
|
159
|
+
expect(stdoutOutput).toContain("Spec written");
|
|
160
|
+
expect(stdoutOutput).toContain("task-attr");
|
|
160
161
|
|
|
161
|
-
const specPath =
|
|
162
|
+
const specPath = join(tempDir, ".overstory", "specs", "task-attr.md");
|
|
162
163
|
const content = await Bun.file(specPath).text();
|
|
163
164
|
expect(content).toContain("<!-- written-by: scout-2 -->");
|
|
164
165
|
expect(content).toContain("# Attributed");
|
|
@@ -167,9 +168,10 @@ describe("specWriteCommand (integration)", () => {
|
|
|
167
168
|
test("writes spec without agent when agent is omitted", async () => {
|
|
168
169
|
await specWriteCommand("task-noagent", { body: "# No Agent" });
|
|
169
170
|
|
|
170
|
-
expect(stdoutOutput
|
|
171
|
+
expect(stdoutOutput).toContain("Spec written");
|
|
172
|
+
expect(stdoutOutput).toContain("task-noagent");
|
|
171
173
|
|
|
172
|
-
const specPath =
|
|
174
|
+
const specPath = join(tempDir, ".overstory", "specs", "task-noagent.md");
|
|
173
175
|
const content = await Bun.file(specPath).text();
|
|
174
176
|
expect(content).not.toContain("written-by");
|
|
175
177
|
expect(content).toBe("# No Agent\n");
|