@os-eco/overstory-cli 0.7.0 → 0.7.3

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.
Files changed (91) hide show
  1. package/README.md +7 -6
  2. package/agents/builder.md +1 -1
  3. package/agents/coordinator.md +12 -11
  4. package/agents/lead.md +6 -6
  5. package/agents/monitor.md +4 -4
  6. package/agents/reviewer.md +1 -1
  7. package/agents/scout.md +5 -5
  8. package/agents/supervisor.md +36 -32
  9. package/package.json +1 -1
  10. package/src/agents/guard-rules.ts +97 -0
  11. package/src/agents/hooks-deployer.test.ts +6 -5
  12. package/src/agents/hooks-deployer.ts +7 -90
  13. package/src/agents/identity.test.ts +3 -2
  14. package/src/agents/manifest.test.ts +4 -3
  15. package/src/agents/overlay.test.ts +10 -9
  16. package/src/agents/overlay.ts +5 -5
  17. package/src/commands/agents.test.ts +10 -4
  18. package/src/commands/clean.test.ts +3 -0
  19. package/src/commands/completions.test.ts +8 -5
  20. package/src/commands/completions.ts +38 -2
  21. package/src/commands/coordinator.test.ts +1 -0
  22. package/src/commands/coordinator.ts +15 -11
  23. package/src/commands/costs.test.ts +9 -3
  24. package/src/commands/dashboard.test.ts +265 -6
  25. package/src/commands/dashboard.ts +367 -64
  26. package/src/commands/doctor.test.ts +3 -2
  27. package/src/commands/errors.test.ts +3 -2
  28. package/src/commands/feed.test.ts +3 -2
  29. package/src/commands/feed.ts +2 -29
  30. package/src/commands/init.test.ts +1 -2
  31. package/src/commands/init.ts +1 -8
  32. package/src/commands/inspect.test.ts +17 -2
  33. package/src/commands/log.test.ts +262 -8
  34. package/src/commands/log.ts +232 -110
  35. package/src/commands/logs.test.ts +3 -2
  36. package/src/commands/mail.test.ts +8 -2
  37. package/src/commands/metrics.test.ts +4 -3
  38. package/src/commands/monitor.ts +15 -11
  39. package/src/commands/nudge.test.ts +4 -2
  40. package/src/commands/prime.test.ts +4 -2
  41. package/src/commands/prime.ts +6 -2
  42. package/src/commands/replay.test.ts +3 -2
  43. package/src/commands/run.test.ts +3 -1
  44. package/src/commands/sling.test.ts +142 -1
  45. package/src/commands/sling.ts +145 -24
  46. package/src/commands/status.test.ts +9 -8
  47. package/src/commands/stop.test.ts +1 -0
  48. package/src/commands/supervisor.ts +19 -12
  49. package/src/commands/trace.test.ts +4 -2
  50. package/src/commands/watch.test.ts +3 -2
  51. package/src/commands/worktree.test.ts +9 -0
  52. package/src/config.test.ts +3 -3
  53. package/src/config.ts +29 -0
  54. package/src/doctor/agents.test.ts +3 -2
  55. package/src/doctor/consistency.test.ts +14 -0
  56. package/src/doctor/logs.test.ts +3 -2
  57. package/src/doctor/structure.test.ts +3 -2
  58. package/src/e2e/init-sling-lifecycle.test.ts +3 -5
  59. package/src/index.ts +3 -1
  60. package/src/logging/color.ts +1 -1
  61. package/src/logging/format.test.ts +110 -0
  62. package/src/logging/format.ts +42 -1
  63. package/src/logging/logger.test.ts +3 -2
  64. package/src/mail/broadcast.test.ts +1 -0
  65. package/src/mail/client.test.ts +3 -2
  66. package/src/mail/store.test.ts +3 -2
  67. package/src/merge/queue.test.ts +3 -2
  68. package/src/merge/resolver.test.ts +39 -0
  69. package/src/merge/resolver.ts +24 -5
  70. package/src/mulch/client.test.ts +63 -2
  71. package/src/mulch/client.ts +62 -1
  72. package/src/runtimes/claude.test.ts +5 -4
  73. package/src/runtimes/pi-guards.test.ts +457 -0
  74. package/src/runtimes/pi-guards.ts +349 -0
  75. package/src/runtimes/pi.test.ts +620 -0
  76. package/src/runtimes/pi.ts +244 -0
  77. package/src/runtimes/registry.test.ts +33 -0
  78. package/src/runtimes/registry.ts +15 -2
  79. package/src/runtimes/types.ts +63 -0
  80. package/src/schema-consistency.test.ts +5 -2
  81. package/src/sessions/compat.test.ts +3 -2
  82. package/src/sessions/compat.ts +1 -0
  83. package/src/sessions/store.test.ts +34 -2
  84. package/src/sessions/store.ts +37 -4
  85. package/src/test-helpers.ts +20 -1
  86. package/src/types.ts +17 -0
  87. package/src/watchdog/daemon.test.ts +11 -7
  88. package/src/watchdog/daemon.ts +1 -1
  89. package/src/watchdog/health.test.ts +1 -0
  90. package/src/watchdog/triage.test.ts +3 -2
  91. package/src/watchdog/triage.ts +14 -4
@@ -15,7 +15,6 @@
15
15
  import { mkdir } from "node:fs/promises";
16
16
  import { join } from "node:path";
17
17
  import { Command } from "commander";
18
- import { deployHooks } from "../agents/hooks-deployer.ts";
19
18
  import { createIdentity, loadIdentity } from "../agents/identity.ts";
20
19
  import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
21
20
  import { loadConfig } from "../config.ts";
@@ -137,8 +136,21 @@ async function startSupervisor(opts: {
137
136
  store.updateState(opts.name, "completed");
138
137
  }
139
138
 
139
+ // Resolve model and runtime early (needed for deployConfig and spawn)
140
+ const manifestLoader = createManifestLoader(
141
+ join(projectRoot, config.agents.manifestPath),
142
+ join(projectRoot, config.agents.baseDir),
143
+ );
144
+ const manifest = await manifestLoader.load();
145
+ const resolvedModel = resolveModel(config, manifest, "supervisor", "opus");
146
+ const runtime = getRuntime(undefined, config);
147
+
140
148
  // Deploy supervisor-specific hooks to the project root's .claude/ directory.
141
- await deployHooks(projectRoot, opts.name, "supervisor");
149
+ await runtime.deployConfig(projectRoot, undefined, {
150
+ agentName: opts.name,
151
+ capability: "supervisor",
152
+ worktreePath: projectRoot,
153
+ });
142
154
 
143
155
  // Create supervisor identity if first run
144
156
  const identityBaseDir = join(projectRoot, ".overstory", "agents");
@@ -155,15 +167,6 @@ async function startSupervisor(opts: {
155
167
  });
156
168
  }
157
169
 
158
- // Resolve model from config > manifest > fallback
159
- const manifestLoader = createManifestLoader(
160
- join(projectRoot, config.agents.manifestPath),
161
- join(projectRoot, config.agents.baseDir),
162
- );
163
- const manifest = await manifestLoader.load();
164
- const resolvedModel = resolveModel(config, manifest, "supervisor", "opus");
165
- const runtime = getRuntime(undefined, config);
166
-
167
170
  // Spawn tmux session at project root with Claude Code (interactive mode).
168
171
  // Inject the supervisor base definition via --append-system-prompt.
169
172
  const tmuxSession = `overstory-${config.project.name}-supervisor-${opts.name}`;
@@ -225,6 +228,7 @@ async function startSupervisor(opts: {
225
228
  lastActivity: new Date().toISOString(),
226
229
  escalationLevel: 0,
227
230
  stalledSince: null,
231
+ transcriptPath: null,
228
232
  };
229
233
 
230
234
  store.upsert(session);
@@ -442,7 +446,7 @@ async function statusSupervisor(opts: { name?: string; json: boolean }): Promise
442
446
  * Create the Commander command for `ov supervisor`.
443
447
  */
444
448
  export function createSupervisorCommand(): Command {
445
- const cmd = new Command("supervisor").description("Manage per-project supervisor agents");
449
+ const cmd = new Command("supervisor").description("[DEPRECATED] Per-project supervisor agent");
446
450
 
447
451
  cmd
448
452
  .command("start")
@@ -460,6 +464,9 @@ export function createSupervisorCommand(): Command {
460
464
  depth: string;
461
465
  json?: boolean;
462
466
  }) => {
467
+ console.error(
468
+ "[DEPRECATED] ov supervisor is deprecated. Use 'ov sling --capability lead' instead.",
469
+ );
463
470
  await startSupervisor({
464
471
  task: opts.task,
465
472
  name: opts.name,
@@ -9,13 +9,14 @@
9
9
  */
10
10
 
11
11
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
12
- import { mkdtemp, rm } from "node:fs/promises";
12
+ import { mkdtemp } from "node:fs/promises";
13
13
  import { tmpdir } from "node:os";
14
14
  import { join } from "node:path";
15
15
  import { ValidationError } from "../errors.ts";
16
16
  import { createEventStore } from "../events/store.ts";
17
17
  import { stripAnsi } from "../logging/color.ts";
18
18
  import { createSessionStore } from "../sessions/store.ts";
19
+ import { cleanupTempDir } from "../test-helpers.ts";
19
20
  import type { InsertEvent } from "../types.ts";
20
21
  import { traceCommand } from "./trace.ts";
21
22
 
@@ -66,7 +67,7 @@ describe("traceCommand", () => {
66
67
  afterEach(async () => {
67
68
  process.stdout.write = originalWrite;
68
69
  process.chdir(originalCwd);
69
- await rm(tempDir, { recursive: true, force: true });
70
+ await cleanupTempDir(tempDir);
70
71
  });
71
72
 
72
73
  function output(): string {
@@ -551,6 +552,7 @@ describe("traceCommand", () => {
551
552
  lastActivity: new Date().toISOString(),
552
553
  escalationLevel: 0,
553
554
  stalledSince: null,
555
+ transcriptPath: null,
554
556
  });
555
557
  sessionStore.close();
556
558
 
@@ -1,7 +1,8 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdtemp, rm } from "node:fs/promises";
2
+ import { mkdtemp } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { cleanupTempDir } from "../test-helpers.ts";
5
6
  import { watchCommand } from "./watch.ts";
6
7
 
7
8
  /**
@@ -66,7 +67,7 @@ describe("watchCommand", () => {
66
67
  process.stderr.write = originalStderrWrite;
67
68
  process.exitCode = originalExitCode;
68
69
  process.chdir(originalCwd);
69
- await rm(tempDir, { recursive: true, force: true });
70
+ await cleanupTempDir(tempDir);
70
71
  });
71
72
 
72
73
  function output(): string {
@@ -79,6 +79,7 @@ describe("worktreeCommand", () => {
79
79
  lastActivity: new Date().toISOString(),
80
80
  escalationLevel: 0,
81
81
  stalledSince: null,
82
+ transcriptPath: null,
82
83
  ...overrides,
83
84
  };
84
85
  }
@@ -167,6 +168,7 @@ describe("worktreeCommand", () => {
167
168
  lastActivity: new Date().toISOString(),
168
169
  escalationLevel: 0,
169
170
  stalledSince: null,
171
+ transcriptPath: null,
170
172
  },
171
173
  ]);
172
174
 
@@ -214,6 +216,7 @@ describe("worktreeCommand", () => {
214
216
  lastActivity: new Date().toISOString(),
215
217
  escalationLevel: 0,
216
218
  stalledSince: null,
219
+ transcriptPath: null,
217
220
  },
218
221
  ]);
219
222
 
@@ -308,6 +311,7 @@ describe("worktreeCommand", () => {
308
311
  lastActivity: new Date().toISOString(),
309
312
  escalationLevel: 0,
310
313
  stalledSince: null,
314
+ transcriptPath: null,
311
315
  },
312
316
  ]);
313
317
 
@@ -363,6 +367,7 @@ describe("worktreeCommand", () => {
363
367
  lastActivity: new Date().toISOString(),
364
368
  escalationLevel: 0,
365
369
  stalledSince: null,
370
+ transcriptPath: null,
366
371
  },
367
372
  ]);
368
373
 
@@ -401,6 +406,7 @@ describe("worktreeCommand", () => {
401
406
  lastActivity: new Date().toISOString(),
402
407
  escalationLevel: 0,
403
408
  stalledSince: null,
409
+ transcriptPath: null,
404
410
  },
405
411
  ]);
406
412
 
@@ -455,6 +461,7 @@ describe("worktreeCommand", () => {
455
461
  lastActivity: new Date().toISOString(),
456
462
  escalationLevel: 0,
457
463
  stalledSince: new Date().toISOString(),
464
+ transcriptPath: null,
458
465
  },
459
466
  ]);
460
467
 
@@ -616,6 +623,7 @@ describe("worktreeCommand", () => {
616
623
  lastActivity: new Date().toISOString(),
617
624
  escalationLevel: 0,
618
625
  stalledSince: null,
626
+ transcriptPath: null,
619
627
  },
620
628
  {
621
629
  id: "session-2",
@@ -634,6 +642,7 @@ describe("worktreeCommand", () => {
634
642
  lastActivity: new Date().toISOString(),
635
643
  escalationLevel: 0,
636
644
  stalledSince: null,
645
+ transcriptPath: null,
637
646
  },
638
647
  ]);
639
648
 
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, realpath, rm } from "node:fs/promises";
2
+ import { mkdir, mkdtemp, realpath } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { DEFAULT_CONFIG, DEFAULT_QUALITY_GATES, loadConfig, resolveProjectRoot } from "./config.ts";
@@ -14,7 +14,7 @@ describe("loadConfig", () => {
14
14
  });
15
15
 
16
16
  afterEach(async () => {
17
- await rm(tempDir, { recursive: true, force: true });
17
+ await cleanupTempDir(tempDir);
18
18
  });
19
19
 
20
20
  async function writeConfig(yaml: string): Promise<void> {
@@ -428,7 +428,7 @@ describe("validateConfig", () => {
428
428
  });
429
429
 
430
430
  afterEach(async () => {
431
- await rm(tempDir, { recursive: true, force: true });
431
+ await cleanupTempDir(tempDir);
432
432
  });
433
433
 
434
434
  async function writeConfig(yaml: string): Promise<void> {
package/src/config.ts CHANGED
@@ -64,6 +64,14 @@ export const DEFAULT_CONFIG: OverstoryConfig = {
64
64
  },
65
65
  runtime: {
66
66
  default: "claude",
67
+ pi: {
68
+ provider: "anthropic",
69
+ modelMap: {
70
+ opus: "anthropic/claude-opus-4-6",
71
+ sonnet: "anthropic/claude-sonnet-4-6",
72
+ haiku: "anthropic/claude-haiku-4-5",
73
+ },
74
+ },
67
75
  },
68
76
  };
69
77
 
@@ -635,6 +643,27 @@ function validateConfig(config: OverstoryConfig): void {
635
643
  );
636
644
  }
637
645
 
646
+ // runtime.pi: validate provider and modelMap if present
647
+ if (config.runtime?.pi) {
648
+ const pi = config.runtime.pi;
649
+ if (!pi.provider || typeof pi.provider !== "string") {
650
+ throw new ValidationError("runtime.pi.provider must be a non-empty string", {
651
+ field: "runtime.pi.provider",
652
+ value: pi.provider,
653
+ });
654
+ }
655
+ if (pi.modelMap && typeof pi.modelMap === "object") {
656
+ for (const [alias, qualified] of Object.entries(pi.modelMap)) {
657
+ if (!qualified || typeof qualified !== "string") {
658
+ throw new ValidationError(`runtime.pi.modelMap.${alias} must be a non-empty string`, {
659
+ field: `runtime.pi.modelMap.${alias}`,
660
+ value: qualified,
661
+ });
662
+ }
663
+ }
664
+ }
665
+ }
666
+
638
667
  // models: validate each value — accepts aliases and provider-prefixed refs
639
668
  const validAliases = ["sonnet", "opus", "haiku"];
640
669
  const toolHeavyRoles = ["builder", "scout"];
@@ -6,9 +6,10 @@
6
6
  */
7
7
 
8
8
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
- import { mkdir, mkdtemp, rm } from "node:fs/promises";
9
+ import { mkdir, mkdtemp } from "node:fs/promises";
10
10
  import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
+ import { cleanupTempDir } from "../test-helpers.ts";
12
13
  import type { OverstoryConfig } from "../types.ts";
13
14
  import { checkAgents } from "./agents.ts";
14
15
 
@@ -74,7 +75,7 @@ describe("checkAgents", () => {
74
75
  });
75
76
 
76
77
  afterEach(async () => {
77
- await rm(tempDir, { recursive: true, force: true });
78
+ await cleanupTempDir(tempDir);
78
79
  });
79
80
 
80
81
  test("fails when manifest is missing", async () => {
@@ -207,6 +207,7 @@ describe("checkConsistency", () => {
207
207
  lastActivity: new Date().toISOString(),
208
208
  escalationLevel: 0,
209
209
  stalledSince: null,
210
+ transcriptPath: null,
210
211
  });
211
212
  store.close();
212
213
 
@@ -243,6 +244,7 @@ describe("checkConsistency", () => {
243
244
  lastActivity: new Date().toISOString(),
244
245
  escalationLevel: 0,
245
246
  stalledSince: null,
247
+ transcriptPath: null,
246
248
  });
247
249
  store.close();
248
250
 
@@ -278,6 +280,7 @@ describe("checkConsistency", () => {
278
280
  lastActivity: new Date().toISOString(),
279
281
  escalationLevel: 0,
280
282
  stalledSince: null,
283
+ transcriptPath: null,
281
284
  });
282
285
  store.close();
283
286
 
@@ -314,6 +317,7 @@ describe("checkConsistency", () => {
314
317
  lastActivity: new Date().toISOString(),
315
318
  escalationLevel: 0,
316
319
  stalledSince: null,
320
+ transcriptPath: null,
317
321
  });
318
322
  store.close();
319
323
 
@@ -353,6 +357,7 @@ describe("checkConsistency", () => {
353
357
  lastActivity: new Date().toISOString(),
354
358
  escalationLevel: 0,
355
359
  stalledSince: null,
360
+ transcriptPath: null,
356
361
  });
357
362
  store.close();
358
363
 
@@ -426,6 +431,7 @@ describe("checkConsistency", () => {
426
431
  lastActivity: new Date().toISOString(),
427
432
  escalationLevel: 0,
428
433
  stalledSince: null,
434
+ transcriptPath: null,
429
435
  });
430
436
 
431
437
  store.upsert({
@@ -445,6 +451,7 @@ describe("checkConsistency", () => {
445
451
  lastActivity: new Date().toISOString(),
446
452
  escalationLevel: 0,
447
453
  stalledSince: null,
454
+ transcriptPath: null,
448
455
  });
449
456
  store.close();
450
457
 
@@ -481,6 +488,7 @@ describe("checkConsistency", () => {
481
488
  lastActivity: new Date().toISOString(),
482
489
  escalationLevel: 0,
483
490
  stalledSince: null,
491
+ transcriptPath: null,
484
492
  });
485
493
  }
486
494
 
@@ -501,6 +509,7 @@ describe("checkConsistency", () => {
501
509
  lastActivity: new Date().toISOString(),
502
510
  escalationLevel: 0,
503
511
  stalledSince: null,
512
+ transcriptPath: null,
504
513
  });
505
514
  store.close();
506
515
 
@@ -535,6 +544,7 @@ describe("checkConsistency", () => {
535
544
  lastActivity: new Date().toISOString(),
536
545
  escalationLevel: 0,
537
546
  stalledSince: null,
547
+ transcriptPath: null,
538
548
  });
539
549
 
540
550
  store.upsert({
@@ -554,6 +564,7 @@ describe("checkConsistency", () => {
554
564
  lastActivity: new Date().toISOString(),
555
565
  escalationLevel: 0,
556
566
  stalledSince: null,
567
+ transcriptPath: null,
557
568
  });
558
569
  }
559
570
  store.close();
@@ -597,6 +608,7 @@ describe("checkConsistency", () => {
597
608
  lastActivity: new Date().toISOString(),
598
609
  escalationLevel: 0,
599
610
  stalledSince: null,
611
+ transcriptPath: null,
600
612
  });
601
613
 
602
614
  store.upsert({
@@ -616,6 +628,7 @@ describe("checkConsistency", () => {
616
628
  lastActivity: new Date().toISOString(),
617
629
  escalationLevel: 0,
618
630
  stalledSince: null,
631
+ transcriptPath: null,
619
632
  });
620
633
 
621
634
  // Lead-2 has builders only (bad)
@@ -636,6 +649,7 @@ describe("checkConsistency", () => {
636
649
  lastActivity: new Date().toISOString(),
637
650
  escalationLevel: 0,
638
651
  stalledSince: null,
652
+ transcriptPath: null,
639
653
  });
640
654
  store.close();
641
655
 
@@ -6,9 +6,10 @@
6
6
  */
7
7
 
8
8
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
9
+ import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
10
10
  import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
+ import { cleanupTempDir } from "../test-helpers.ts";
12
13
  import type { OverstoryConfig } from "../types.ts";
13
14
  import { checkLogs } from "./logs.ts";
14
15
 
@@ -77,7 +78,7 @@ describe("checkLogs", () => {
77
78
  });
78
79
 
79
80
  afterEach(async () => {
80
- await rm(tempDir, { recursive: true, force: true });
81
+ await cleanupTempDir(tempDir);
81
82
  });
82
83
 
83
84
  test("warns when logs/ directory does not exist", async () => {
@@ -6,9 +6,10 @@
6
6
  */
7
7
 
8
8
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
- import { mkdir, mkdtemp, rm, utimes } from "node:fs/promises";
9
+ import { mkdir, mkdtemp, utimes } from "node:fs/promises";
10
10
  import { tmpdir } from "node:os";
11
11
  import { join } from "node:path";
12
+ import { cleanupTempDir } from "../test-helpers.ts";
12
13
  import type { OverstoryConfig } from "../types.ts";
13
14
  import { checkStructure } from "./structure.ts";
14
15
 
@@ -73,7 +74,7 @@ describe("checkStructure", () => {
73
74
  });
74
75
 
75
76
  afterEach(async () => {
76
- await rm(tempDir, { recursive: true, force: true });
77
+ await cleanupTempDir(tempDir);
77
78
  });
78
79
 
79
80
  test("fails when .overstory/ directory does not exist", async () => {
@@ -27,7 +27,6 @@ const EXPECTED_AGENT_DEFS = [
27
27
  "monitor.md",
28
28
  "reviewer.md",
29
29
  "scout.md",
30
- "supervisor.md",
31
30
  ];
32
31
 
33
32
  describe("E2E: init→sling lifecycle on external project", () => {
@@ -77,7 +76,7 @@ describe("E2E: init→sling lifecycle on external project", () => {
77
76
  const gitignoreFile = Bun.file(join(overstoryDir, ".gitignore"));
78
77
  expect(await gitignoreFile.exists()).toBe(true);
79
78
 
80
- // agent-defs/ contains all 8 agent definition files
79
+ // agent-defs/ contains all 7 agent definition files (supervisor deprecated)
81
80
  const agentDefsDir = join(overstoryDir, "agent-defs");
82
81
  const agentDefFiles = (await readdir(agentDefsDir)).filter((f) => f.endsWith(".md")).sort();
83
82
  expect(agentDefFiles).toEqual(EXPECTED_AGENT_DEFS);
@@ -109,7 +108,7 @@ describe("E2E: init→sling lifecycle on external project", () => {
109
108
  expect(config.project.name).toBeTruthy();
110
109
  });
111
110
 
112
- test("manifest loads successfully with all 8 agents", async () => {
111
+ test("manifest loads successfully with all 7 agents (supervisor deprecated)", async () => {
113
112
  await initCommand({});
114
113
 
115
114
  const manifestPath = join(tempDir, ".overstory", "agent-manifest.json");
@@ -118,7 +117,7 @@ describe("E2E: init→sling lifecycle on external project", () => {
118
117
 
119
118
  const manifest = await loader.load();
120
119
 
121
- // All 8 agents present
120
+ // All 7 agents present (supervisor removed: deprecated, use lead instead)
122
121
  const agentNames = Object.keys(manifest.agents).sort();
123
122
  expect(agentNames).toEqual([
124
123
  "builder",
@@ -128,7 +127,6 @@ describe("E2E: init→sling lifecycle on external project", () => {
128
127
  "monitor",
129
128
  "reviewer",
130
129
  "scout",
131
- "supervisor",
132
130
  ]);
133
131
 
134
132
  // Each agent has a valid file reference
package/src/index.ts CHANGED
@@ -45,7 +45,7 @@ import { OverstoryError, WorktreeError } from "./errors.ts";
45
45
  import { jsonError } from "./json.ts";
46
46
  import { brand, chalk, muted, setQuiet } from "./logging/color.ts";
47
47
 
48
- export const VERSION = "0.7.0";
48
+ export const VERSION = "0.7.3";
49
49
 
50
50
  const rawArgs = process.argv.slice(2);
51
51
 
@@ -96,6 +96,7 @@ const COMMANDS = [
96
96
  "costs",
97
97
  "metrics",
98
98
  "upgrade",
99
+ "completions",
99
100
  ];
100
101
 
101
102
  function editDistance(a: string, b: string): number {
@@ -254,6 +255,7 @@ program
254
255
  .option("--force-hierarchy", "Bypass hierarchy validation")
255
256
  .option("--max-agents <n>", "Max children per lead (overrides config)")
256
257
  .option("--skip-review", "Skip review phase for lead agents")
258
+ .option("--no-scout-check", "Suppress the parentHasScouts scout-before-build warning")
257
259
  .option("--dispatch-max-agents <n>", "Per-lead max agents ceiling (injected into overlay)")
258
260
  .option("--runtime <name>", "Runtime adapter (default: config or claude)")
259
261
  .option("--json", "Output result as JSON")
@@ -10,7 +10,7 @@ import chalk from "chalk";
10
10
  // --- Brand palette (os-eco brand colors) ---
11
11
 
12
12
  /** Forest green — Overstory primary brand color. */
13
- export const brand = chalk.rgb(27, 94, 32);
13
+ export const brand = chalk.rgb(46, 125, 50);
14
14
 
15
15
  /** Amber — highlights, warnings. */
16
16
  export const accent = chalk.rgb(255, 183, 77);
@@ -0,0 +1,110 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { StoredEvent } from "../types.ts";
3
+ import { stripAnsi } from "./color.ts";
4
+ import { formatEventLine, numericPriorityColor } from "./format.ts";
5
+
6
+ // Minimal StoredEvent fixture for testing formatEventLine
7
+ const BASE_EVENT: StoredEvent = {
8
+ id: 1,
9
+ runId: "run-001",
10
+ agentName: "agent1",
11
+ sessionId: null,
12
+ eventType: "tool_start",
13
+ level: "info",
14
+ toolName: "bash",
15
+ toolArgs: null,
16
+ toolDurationMs: null,
17
+ data: null,
18
+ createdAt: "2024-01-15T10:30:45.000Z",
19
+ };
20
+
21
+ describe("numericPriorityColor", () => {
22
+ test("returns a function for priority 1", () => {
23
+ const fn = numericPriorityColor(1);
24
+ expect(typeof fn).toBe("function");
25
+ // Function must accept a string and return a string
26
+ expect(typeof fn("x")).toBe("string");
27
+ });
28
+
29
+ test("returns a function for priority 2", () => {
30
+ const fn = numericPriorityColor(2);
31
+ expect(typeof fn).toBe("function");
32
+ expect(typeof fn("x")).toBe("string");
33
+ });
34
+
35
+ test("priority 3 is identity (returns input unchanged)", () => {
36
+ const fn = numericPriorityColor(3);
37
+ // Priority 3 = normal = (text) => text, always identity regardless of chalk level
38
+ expect(fn("x")).toBe("x");
39
+ expect(fn("hello world")).toBe("hello world");
40
+ });
41
+
42
+ test("returns a function for priority 4", () => {
43
+ const fn = numericPriorityColor(4);
44
+ expect(typeof fn).toBe("function");
45
+ expect(typeof fn("x")).toBe("string");
46
+ });
47
+
48
+ test("unknown priority returns identity function", () => {
49
+ const fn = numericPriorityColor(99);
50
+ expect(typeof fn).toBe("function");
51
+ expect(fn("x")).toBe("x");
52
+ });
53
+
54
+ test("all priority functions preserve input text (visible content after strip)", () => {
55
+ for (const p of [1, 2, 3, 4]) {
56
+ const fn = numericPriorityColor(p);
57
+ expect(stripAnsi(fn("hello"))).toBe("hello");
58
+ }
59
+ });
60
+ });
61
+
62
+ describe("formatEventLine", () => {
63
+ test("returns a non-empty string", () => {
64
+ const colorMap = new Map<string, (t: string) => string>();
65
+ const result = formatEventLine(BASE_EVENT, colorMap);
66
+ expect(result.length).toBeGreaterThan(0);
67
+ });
68
+
69
+ test("includes agent name in the result", () => {
70
+ const colorMap = new Map<string, (t: string) => string>();
71
+ const result = formatEventLine(BASE_EVENT, colorMap);
72
+ // Strip ANSI so we can do plain text comparison
73
+ expect(stripAnsi(result)).toContain("agent1");
74
+ });
75
+
76
+ test("includes time portion (10:30:45) in the result", () => {
77
+ const colorMap = new Map<string, (t: string) => string>();
78
+ const result = formatEventLine(BASE_EVENT, colorMap);
79
+ expect(stripAnsi(result)).toContain("10:30:45");
80
+ });
81
+
82
+ test("result does NOT end with a newline", () => {
83
+ const colorMap = new Map<string, (t: string) => string>();
84
+ const result = formatEventLine(BASE_EVENT, colorMap);
85
+ expect(result.endsWith("\n")).toBe(false);
86
+ });
87
+
88
+ test("error level result is a non-empty string", () => {
89
+ const colorMap = new Map<string, (t: string) => string>();
90
+ const errorEvent: StoredEvent = { ...BASE_EVENT, level: "error" };
91
+ const result = formatEventLine(errorEvent, colorMap);
92
+ expect(result.length).toBeGreaterThan(0);
93
+ expect(result.endsWith("\n")).toBe(false);
94
+ });
95
+
96
+ test("uses color from colorMap when agent is registered", () => {
97
+ const blueColor = (t: string) => `\x1b[34m${t}\x1b[39m`;
98
+ const colorMap = new Map<string, (t: string) => string>([["agent1", blueColor]]);
99
+ const result = formatEventLine(BASE_EVENT, colorMap);
100
+ // The colored agent name should appear in the output
101
+ expect(result).toContain("\x1b[34m");
102
+ });
103
+
104
+ test("falls back to gray when agent is not in colorMap", () => {
105
+ const colorMap = new Map<string, (t: string) => string>();
106
+ // Should not throw — gray fallback is used
107
+ const result = formatEventLine(BASE_EVENT, colorMap);
108
+ expect(result.length).toBeGreaterThan(0);
109
+ });
110
+ });
@@ -8,7 +8,7 @@
8
8
  import type { StoredEvent } from "../types.ts";
9
9
  import type { ColorFn } from "./color.ts";
10
10
  import { color, noColor } from "./color.ts";
11
- import { AGENT_COLORS } from "./theme.ts";
11
+ import { AGENT_COLORS, eventLabel } from "./theme.ts";
12
12
 
13
13
  // === Duration ===
14
14
 
@@ -175,6 +175,25 @@ export function priorityColor(priority: string): ColorFn {
175
175
  }
176
176
  }
177
177
 
178
+ /**
179
+ * Returns a color function for a numeric tracker priority.
180
+ * 1=urgent (red), 2=high (yellow), 3=normal (identity), 4=low (dim)
181
+ */
182
+ export function numericPriorityColor(priority: number): ColorFn {
183
+ switch (priority) {
184
+ case 1:
185
+ return color.red;
186
+ case 2:
187
+ return color.yellow;
188
+ case 3:
189
+ return (text: string) => text;
190
+ case 4:
191
+ return color.dim;
192
+ default:
193
+ return (text: string) => text;
194
+ }
195
+ }
196
+
178
197
  /**
179
198
  * Returns a color function for a log level string.
180
199
  * debug=gray, info=blue, warn=yellow, error=red
@@ -212,3 +231,25 @@ export function logLevelLabel(level: string): string {
212
231
  return level.slice(0, 3).toUpperCase();
213
232
  }
214
233
  }
234
+
235
+ /**
236
+ * Format a single event as a compact feed line.
237
+ * Returns the formatted string WITHOUT a trailing newline.
238
+ * Used by both ov feed and the dashboard Feed panel.
239
+ */
240
+ export function formatEventLine(event: StoredEvent, colorMap: Map<string, ColorFn>): string {
241
+ const timeStr = formatAbsoluteTime(event.createdAt);
242
+ const label = eventLabel(event.eventType);
243
+ const levelColorFn =
244
+ event.level === "error" ? color.red : event.level === "warn" ? color.yellow : null;
245
+ const applyLevel = (text: string) => (levelColorFn ? levelColorFn(text) : text);
246
+ const detail = buildEventDetail(event, 60);
247
+ const detailSuffix = detail ? ` ${color.dim(detail)}` : "";
248
+ const agentColorFn = colorMap.get(event.agentName) ?? color.gray;
249
+ const agentLabel = ` ${agentColorFn(event.agentName.padEnd(15))}`;
250
+ return (
251
+ `${color.dim(timeStr)} ` +
252
+ `${applyLevel(label.color(color.bold(label.compact)))}` +
253
+ `${agentLabel}${detailSuffix}`
254
+ );
255
+ }