@os-eco/overstory-cli 0.7.2 → 0.7.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 +21 -9
- package/agents/builder.md +6 -0
- package/agents/coordinator.md +2 -2
- package/agents/lead.md +4 -1
- package/agents/merger.md +3 -2
- package/agents/monitor.md +1 -1
- package/agents/reviewer.md +1 -0
- package/agents/scout.md +1 -0
- package/package.json +2 -2
- package/src/agents/hooks-deployer.test.ts +6 -5
- package/src/agents/identity.test.ts +3 -2
- package/src/agents/manifest.test.ts +4 -3
- package/src/agents/overlay.test.ts +3 -2
- package/src/commands/agents.test.ts +5 -4
- package/src/commands/agents.ts +18 -8
- package/src/commands/completions.test.ts +8 -5
- package/src/commands/completions.ts +37 -1
- package/src/commands/costs.test.ts +4 -3
- package/src/commands/dashboard.test.ts +265 -6
- package/src/commands/dashboard.ts +367 -64
- package/src/commands/doctor.test.ts +3 -2
- package/src/commands/errors.test.ts +3 -2
- package/src/commands/feed.test.ts +3 -2
- package/src/commands/feed.ts +2 -29
- package/src/commands/inspect.test.ts +3 -2
- package/src/commands/log.test.ts +248 -8
- package/src/commands/log.ts +193 -110
- package/src/commands/logs.test.ts +3 -2
- package/src/commands/mail.test.ts +3 -2
- package/src/commands/metrics.test.ts +4 -3
- package/src/commands/nudge.test.ts +3 -2
- package/src/commands/prime.test.ts +3 -2
- package/src/commands/prime.ts +1 -16
- package/src/commands/replay.test.ts +3 -2
- package/src/commands/run.test.ts +2 -1
- package/src/commands/sling.test.ts +127 -0
- package/src/commands/sling.ts +101 -3
- package/src/commands/status.test.ts +8 -8
- package/src/commands/trace.test.ts +3 -2
- package/src/commands/watch.test.ts +3 -2
- package/src/config.test.ts +3 -3
- package/src/doctor/agents.test.ts +3 -2
- package/src/doctor/logs.test.ts +3 -2
- package/src/doctor/structure.test.ts +3 -2
- package/src/index.ts +3 -1
- package/src/logging/color.ts +1 -1
- package/src/logging/format.test.ts +110 -0
- package/src/logging/format.ts +42 -1
- package/src/logging/logger.test.ts +3 -2
- package/src/mail/client.test.ts +3 -2
- package/src/mail/store.test.ts +3 -2
- package/src/merge/queue.test.ts +3 -2
- package/src/merge/resolver.test.ts +39 -0
- package/src/merge/resolver.ts +1 -1
- package/src/metrics/pricing.ts +80 -0
- package/src/metrics/transcript.test.ts +58 -1
- package/src/metrics/transcript.ts +9 -68
- package/src/mulch/client.test.ts +63 -2
- package/src/mulch/client.ts +62 -1
- package/src/runtimes/claude.test.ts +4 -3
- package/src/runtimes/pi-guards.test.ts +55 -2
- package/src/runtimes/pi-guards.ts +26 -9
- package/src/schema-consistency.test.ts +4 -2
- package/src/sessions/compat.test.ts +3 -2
- package/src/sessions/store.test.ts +3 -2
- package/src/test-helpers.ts +20 -1
- package/src/tracker/beads.test.ts +454 -0
- package/src/tracker/seeds.test.ts +461 -0
- package/src/watchdog/daemon.test.ts +4 -3
- package/src/watchdog/triage.test.ts +3 -2
|
@@ -7,18 +7,27 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
-
import { mkdir, mkdtemp
|
|
10
|
+
import { mkdir, mkdtemp } from "node:fs/promises";
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { ValidationError } from "../errors.ts";
|
|
14
|
+
import { createEventStore } from "../events/store.ts";
|
|
15
|
+
import { color } from "../logging/color.ts";
|
|
14
16
|
import { createSessionStore } from "../sessions/store.ts";
|
|
17
|
+
import { cleanupTempDir } from "../test-helpers.ts";
|
|
18
|
+
import type { DashboardStores } from "./dashboard.ts";
|
|
15
19
|
import {
|
|
16
20
|
closeDashboardStores,
|
|
21
|
+
computeAgentPanelHeight,
|
|
17
22
|
dashboardCommand,
|
|
23
|
+
dimBox,
|
|
18
24
|
filterAgentsByRun,
|
|
19
25
|
horizontalLine,
|
|
20
26
|
openDashboardStores,
|
|
21
27
|
pad,
|
|
28
|
+
renderAgentPanel,
|
|
29
|
+
renderFeedPanel,
|
|
30
|
+
renderTasksPanel,
|
|
22
31
|
truncate,
|
|
23
32
|
} from "./dashboard.ts";
|
|
24
33
|
|
|
@@ -40,7 +49,7 @@ describe("dashboardCommand", () => {
|
|
|
40
49
|
|
|
41
50
|
afterEach(async () => {
|
|
42
51
|
process.stdout.write = originalWrite;
|
|
43
|
-
await
|
|
52
|
+
await cleanupTempDir(tempDir);
|
|
44
53
|
});
|
|
45
54
|
|
|
46
55
|
function output(): string {
|
|
@@ -203,6 +212,202 @@ describe("filterAgentsByRun", () => {
|
|
|
203
212
|
});
|
|
204
213
|
});
|
|
205
214
|
|
|
215
|
+
describe("dimBox", () => {
|
|
216
|
+
test("dimBox.vertical equals color.dim(│)", () => {
|
|
217
|
+
expect(dimBox.vertical).toBe(color.dim("│"));
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("dimBox.horizontal equals color.dim(─)", () => {
|
|
221
|
+
expect(dimBox.horizontal).toBe(color.dim("─"));
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("dimBox.tee equals color.dim(├)", () => {
|
|
225
|
+
expect(dimBox.tee).toBe(color.dim("├"));
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("dimBox.teeRight equals color.dim(┤)", () => {
|
|
229
|
+
expect(dimBox.teeRight).toBe(color.dim("┤"));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("dimBox values equal color.dim() applied to their characters", () => {
|
|
233
|
+
// dimBox values are always equal to color.dim(char) regardless of whether
|
|
234
|
+
// Chalk emits ANSI codes (it may suppress them in non-TTY / NO_COLOR envs).
|
|
235
|
+
expect(dimBox.topLeft).toBe(color.dim("┌"));
|
|
236
|
+
expect(dimBox.topRight).toBe(color.dim("┐"));
|
|
237
|
+
expect(dimBox.bottomLeft).toBe(color.dim("└"));
|
|
238
|
+
expect(dimBox.bottomRight).toBe(color.dim("┘"));
|
|
239
|
+
expect(dimBox.cross).toBe(color.dim("┼"));
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe("computeAgentPanelHeight", () => {
|
|
244
|
+
test("0 agents: clamps to minimum 8", () => {
|
|
245
|
+
// max(8, min(floor(30*0.5), 0+4)) = max(8, min(15,4)) = max(8,4) = 8
|
|
246
|
+
expect(computeAgentPanelHeight(30, 0)).toBe(8);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("4 agents: still clamps to minimum 8", () => {
|
|
250
|
+
// max(8, min(15, 4+4)) = max(8, 8) = 8
|
|
251
|
+
expect(computeAgentPanelHeight(30, 4)).toBe(8);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("20 agents with height 30: clamps to floor(height*0.5)", () => {
|
|
255
|
+
// max(8, min(15, 24)) = max(8,15) = 15
|
|
256
|
+
expect(computeAgentPanelHeight(30, 20)).toBe(15);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("10 agents with height 30: grows with agent count", () => {
|
|
260
|
+
// max(8, min(15, 14)) = max(8,14) = 14
|
|
261
|
+
expect(computeAgentPanelHeight(30, 10)).toBe(14);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("small height: respects 50% cap", () => {
|
|
265
|
+
// height=20: max(8, min(10, 20+4)) = max(8,10) = 10
|
|
266
|
+
expect(computeAgentPanelHeight(20, 20)).toBe(10);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Helper to build a minimal DashboardData for panel tests
|
|
271
|
+
function makeDashboardData(
|
|
272
|
+
overrides: Partial<{
|
|
273
|
+
tasks: Array<{ id: string; title: string; priority: number; status: string; type: string }>;
|
|
274
|
+
recentEvents: Array<{
|
|
275
|
+
id: number;
|
|
276
|
+
agentName: string;
|
|
277
|
+
eventType: string;
|
|
278
|
+
level: string;
|
|
279
|
+
createdAt: string;
|
|
280
|
+
runId: null;
|
|
281
|
+
sessionId: null;
|
|
282
|
+
toolName: null;
|
|
283
|
+
toolArgs: null;
|
|
284
|
+
toolDurationMs: null;
|
|
285
|
+
data: null;
|
|
286
|
+
}>;
|
|
287
|
+
}> = {},
|
|
288
|
+
) {
|
|
289
|
+
return {
|
|
290
|
+
currentRunId: null,
|
|
291
|
+
status: {
|
|
292
|
+
currentRunId: null,
|
|
293
|
+
agents: [],
|
|
294
|
+
worktrees: [],
|
|
295
|
+
tmuxSessions: [],
|
|
296
|
+
unreadMailCount: 0,
|
|
297
|
+
mergeQueueCount: 0,
|
|
298
|
+
recentMetricsCount: 0,
|
|
299
|
+
},
|
|
300
|
+
recentMail: [],
|
|
301
|
+
mergeQueue: [],
|
|
302
|
+
metrics: { totalSessions: 0, avgDuration: 0, byCapability: {} },
|
|
303
|
+
tasks: overrides.tasks ?? [],
|
|
304
|
+
recentEvents: (overrides.recentEvents as never[]) ?? [],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
describe("renderTasksPanel", () => {
|
|
309
|
+
test("renders task id in output", () => {
|
|
310
|
+
const data = makeDashboardData({
|
|
311
|
+
tasks: [{ id: "t1", title: "Test task", priority: 2, status: "open", type: "task" }],
|
|
312
|
+
});
|
|
313
|
+
const out = renderTasksPanel(data, 1, 80, 10, 1);
|
|
314
|
+
expect(out).toContain("t1");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("renders task title in output", () => {
|
|
318
|
+
const data = makeDashboardData({
|
|
319
|
+
tasks: [{ id: "t1", title: "Test task", priority: 2, status: "open", type: "task" }],
|
|
320
|
+
});
|
|
321
|
+
const out = renderTasksPanel(data, 1, 80, 10, 1);
|
|
322
|
+
expect(out).toContain("Test task");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("renders priority label in output", () => {
|
|
326
|
+
const data = makeDashboardData({
|
|
327
|
+
tasks: [{ id: "t1", title: "Test task", priority: 2, status: "open", type: "task" }],
|
|
328
|
+
});
|
|
329
|
+
const out = renderTasksPanel(data, 1, 80, 10, 1);
|
|
330
|
+
expect(out).toContain("P2");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("shows 'No tracker data' when tasks list is empty", () => {
|
|
334
|
+
const data = makeDashboardData({ tasks: [] });
|
|
335
|
+
const out = renderTasksPanel(data, 1, 80, 10, 1);
|
|
336
|
+
expect(out).toContain("No tracker data");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("renders Tasks header", () => {
|
|
340
|
+
const data = makeDashboardData({ tasks: [] });
|
|
341
|
+
const out = renderTasksPanel(data, 1, 80, 6, 1);
|
|
342
|
+
expect(out).toContain("Tasks");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("renders multiple tasks", () => {
|
|
346
|
+
const data = makeDashboardData({
|
|
347
|
+
tasks: [
|
|
348
|
+
{ id: "abc-001", title: "First task", priority: 1, status: "open", type: "task" },
|
|
349
|
+
{ id: "abc-002", title: "Second task", priority: 3, status: "in_progress", type: "bug" },
|
|
350
|
+
],
|
|
351
|
+
});
|
|
352
|
+
const out = renderTasksPanel(data, 1, 80, 10, 1);
|
|
353
|
+
expect(out).toContain("abc-001");
|
|
354
|
+
expect(out).toContain("abc-002");
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
describe("renderFeedPanel", () => {
|
|
359
|
+
test("shows 'No recent events' when recentEvents is empty", () => {
|
|
360
|
+
const data = makeDashboardData({ recentEvents: [] });
|
|
361
|
+
const out = renderFeedPanel(data, 1, 80, 8, 1);
|
|
362
|
+
expect(out).toContain("No recent events");
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("renders Feed header", () => {
|
|
366
|
+
const data = makeDashboardData({ recentEvents: [] });
|
|
367
|
+
const out = renderFeedPanel(data, 1, 80, 8, 1);
|
|
368
|
+
expect(out).toContain("Feed");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("renders event agent name when events are present", () => {
|
|
372
|
+
const event = {
|
|
373
|
+
id: 1,
|
|
374
|
+
agentName: "test-agent",
|
|
375
|
+
eventType: "tool_end" as const,
|
|
376
|
+
level: "info" as const,
|
|
377
|
+
createdAt: new Date().toISOString(),
|
|
378
|
+
runId: null,
|
|
379
|
+
sessionId: null,
|
|
380
|
+
toolName: null,
|
|
381
|
+
toolArgs: null,
|
|
382
|
+
toolDurationMs: null,
|
|
383
|
+
data: null,
|
|
384
|
+
};
|
|
385
|
+
const data = makeDashboardData({ recentEvents: [event] });
|
|
386
|
+
// formatEventLine is a stub — returns "" — so output won't have agent name from it.
|
|
387
|
+
// But the panel itself should not throw and should render the border structure.
|
|
388
|
+
const out = renderFeedPanel(data, 1, 80, 8, 1);
|
|
389
|
+
// Panel renders without error and contains Feed header
|
|
390
|
+
expect(out).toContain("Feed");
|
|
391
|
+
// At least 1 row rendered (not the "No recent events" path)
|
|
392
|
+
expect(out).not.toContain("No recent events");
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe("renderAgentPanel", () => {
|
|
397
|
+
test("renders Agents header", () => {
|
|
398
|
+
const data = makeDashboardData({});
|
|
399
|
+
const out = renderAgentPanel(data, 100, 12, 3);
|
|
400
|
+
expect(out).toContain("Agents");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("renders with dimmed border characters", () => {
|
|
404
|
+
const data = makeDashboardData({});
|
|
405
|
+
const out = renderAgentPanel(data, 100, 12, 3);
|
|
406
|
+
// dimBox.vertical is a dimmed ANSI string — present in output
|
|
407
|
+
expect(out).toContain(dimBox.vertical);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
206
411
|
describe("openDashboardStores", () => {
|
|
207
412
|
let tempDir: string;
|
|
208
413
|
|
|
@@ -211,14 +416,12 @@ describe("openDashboardStores", () => {
|
|
|
211
416
|
});
|
|
212
417
|
|
|
213
418
|
afterEach(async () => {
|
|
214
|
-
await
|
|
419
|
+
await cleanupTempDir(tempDir);
|
|
215
420
|
});
|
|
216
421
|
|
|
217
422
|
test("sessionStore is non-null when .overstory/ has sessions.db", async () => {
|
|
218
|
-
// Create the .overstory directory and seed a sessions.db via createSessionStore
|
|
219
423
|
const overstoryDir = join(tempDir, ".overstory");
|
|
220
424
|
await mkdir(overstoryDir, { recursive: true });
|
|
221
|
-
// createSessionStore creates and initialises sessions.db
|
|
222
425
|
const seeder = createSessionStore(join(overstoryDir, "sessions.db"));
|
|
223
426
|
seeder.close();
|
|
224
427
|
|
|
@@ -271,6 +474,38 @@ describe("openDashboardStores", () => {
|
|
|
271
474
|
closeDashboardStores(stores);
|
|
272
475
|
}
|
|
273
476
|
});
|
|
477
|
+
|
|
478
|
+
test("eventStore is null when events.db does not exist", async () => {
|
|
479
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
480
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
481
|
+
const seeder = createSessionStore(join(overstoryDir, "sessions.db"));
|
|
482
|
+
seeder.close();
|
|
483
|
+
|
|
484
|
+
const stores = openDashboardStores(tempDir);
|
|
485
|
+
try {
|
|
486
|
+
expect(stores.eventStore).toBeNull();
|
|
487
|
+
} finally {
|
|
488
|
+
closeDashboardStores(stores);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("eventStore is non-null when events.db exists", async () => {
|
|
493
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
494
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
495
|
+
const seeder = createSessionStore(join(overstoryDir, "sessions.db"));
|
|
496
|
+
seeder.close();
|
|
497
|
+
|
|
498
|
+
// Create events.db via createEventStore
|
|
499
|
+
const eventsDb = createEventStore(join(overstoryDir, "events.db"));
|
|
500
|
+
eventsDb.close();
|
|
501
|
+
|
|
502
|
+
const stores = openDashboardStores(tempDir);
|
|
503
|
+
try {
|
|
504
|
+
expect(stores.eventStore).not.toBeNull();
|
|
505
|
+
} finally {
|
|
506
|
+
closeDashboardStores(stores);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
274
509
|
});
|
|
275
510
|
|
|
276
511
|
describe("closeDashboardStores", () => {
|
|
@@ -281,7 +516,7 @@ describe("closeDashboardStores", () => {
|
|
|
281
516
|
});
|
|
282
517
|
|
|
283
518
|
afterEach(async () => {
|
|
284
|
-
await
|
|
519
|
+
await cleanupTempDir(tempDir);
|
|
285
520
|
});
|
|
286
521
|
|
|
287
522
|
test("closing stores does not throw", async () => {
|
|
@@ -305,4 +540,28 @@ describe("closeDashboardStores", () => {
|
|
|
305
540
|
// Second close should not throw due to best-effort try/catch
|
|
306
541
|
expect(() => closeDashboardStores(stores)).not.toThrow();
|
|
307
542
|
});
|
|
543
|
+
|
|
544
|
+
test("closing stores with eventStore does not throw", async () => {
|
|
545
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
546
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
547
|
+
const seeder = createSessionStore(join(overstoryDir, "sessions.db"));
|
|
548
|
+
seeder.close();
|
|
549
|
+
const eventsDb = createEventStore(join(overstoryDir, "events.db"));
|
|
550
|
+
eventsDb.close();
|
|
551
|
+
|
|
552
|
+
const stores = openDashboardStores(tempDir);
|
|
553
|
+
expect(() => closeDashboardStores(stores)).not.toThrow();
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// Type check: DashboardStores includes eventStore
|
|
558
|
+
test("DashboardStores type includes eventStore field", () => {
|
|
559
|
+
const stores: DashboardStores = {
|
|
560
|
+
sessionStore: null as never,
|
|
561
|
+
mailStore: null,
|
|
562
|
+
mergeQueue: null,
|
|
563
|
+
metricsStore: null,
|
|
564
|
+
eventStore: null,
|
|
565
|
+
};
|
|
566
|
+
expect(stores.eventStore).toBeNull();
|
|
308
567
|
});
|