@os-eco/overstory-cli 0.7.0 → 0.7.2

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 (60) hide show
  1. package/README.md +6 -5
  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.ts +7 -90
  12. package/src/agents/overlay.test.ts +7 -7
  13. package/src/agents/overlay.ts +5 -5
  14. package/src/commands/agents.test.ts +5 -0
  15. package/src/commands/clean.test.ts +3 -0
  16. package/src/commands/completions.ts +1 -1
  17. package/src/commands/coordinator.test.ts +1 -0
  18. package/src/commands/coordinator.ts +15 -11
  19. package/src/commands/costs.test.ts +5 -0
  20. package/src/commands/init.test.ts +1 -2
  21. package/src/commands/init.ts +1 -8
  22. package/src/commands/inspect.test.ts +14 -0
  23. package/src/commands/log.test.ts +14 -0
  24. package/src/commands/log.ts +39 -0
  25. package/src/commands/mail.test.ts +5 -0
  26. package/src/commands/monitor.ts +15 -11
  27. package/src/commands/nudge.test.ts +1 -0
  28. package/src/commands/prime.test.ts +2 -0
  29. package/src/commands/prime.ts +6 -2
  30. package/src/commands/run.test.ts +1 -0
  31. package/src/commands/sling.test.ts +15 -1
  32. package/src/commands/sling.ts +44 -21
  33. package/src/commands/status.test.ts +1 -0
  34. package/src/commands/stop.test.ts +1 -0
  35. package/src/commands/supervisor.ts +19 -12
  36. package/src/commands/trace.test.ts +1 -0
  37. package/src/commands/worktree.test.ts +9 -0
  38. package/src/config.ts +29 -0
  39. package/src/doctor/consistency.test.ts +14 -0
  40. package/src/e2e/init-sling-lifecycle.test.ts +3 -5
  41. package/src/index.ts +1 -1
  42. package/src/mail/broadcast.test.ts +1 -0
  43. package/src/merge/resolver.ts +23 -4
  44. package/src/runtimes/claude.test.ts +1 -1
  45. package/src/runtimes/pi-guards.test.ts +433 -0
  46. package/src/runtimes/pi-guards.ts +349 -0
  47. package/src/runtimes/pi.test.ts +620 -0
  48. package/src/runtimes/pi.ts +244 -0
  49. package/src/runtimes/registry.test.ts +33 -0
  50. package/src/runtimes/registry.ts +15 -2
  51. package/src/runtimes/types.ts +63 -0
  52. package/src/schema-consistency.test.ts +1 -0
  53. package/src/sessions/compat.ts +1 -0
  54. package/src/sessions/store.test.ts +31 -0
  55. package/src/sessions/store.ts +37 -4
  56. package/src/types.ts +17 -0
  57. package/src/watchdog/daemon.test.ts +7 -4
  58. package/src/watchdog/daemon.ts +1 -1
  59. package/src/watchdog/health.test.ts +1 -0
  60. package/src/watchdog/triage.ts +14 -4
@@ -176,6 +176,23 @@ async function resolveTranscriptPath(
176
176
  logsBase: string,
177
177
  agentName: string,
178
178
  ): Promise<string | null> {
179
+ // Check SessionStore for a runtime-provided transcript path
180
+ try {
181
+ const { store } = openSessionStore(join(projectRoot, ".overstory"));
182
+ try {
183
+ const session = store.getByName(agentName);
184
+ if (session?.transcriptPath) {
185
+ if (await Bun.file(session.transcriptPath).exists()) {
186
+ return session.transcriptPath;
187
+ }
188
+ }
189
+ } finally {
190
+ store.close();
191
+ }
192
+ } catch {
193
+ // Non-fatal: fall through to legacy resolution
194
+ }
195
+
179
196
  // Check cached path first
180
197
  const cachePath = join(logsBase, agentName, ".transcript-path");
181
198
  const cacheFile = Bun.file(cachePath);
@@ -194,6 +211,17 @@ async function resolveTranscriptPath(
194
211
  const directPath = join(claudeProjectsDir, projectKey, `${sessionId}.jsonl`);
195
212
  if (await Bun.file(directPath).exists()) {
196
213
  await Bun.write(cachePath, directPath);
214
+ // Save discovered path to SessionStore for future lookups
215
+ try {
216
+ const { store: writeStore } = openSessionStore(join(projectRoot, ".overstory"));
217
+ try {
218
+ writeStore.updateTranscriptPath(agentName, directPath);
219
+ } finally {
220
+ writeStore.close();
221
+ }
222
+ } catch {
223
+ // Non-fatal: cache write failure should not break transcript resolution
224
+ }
197
225
  return directPath;
198
226
  }
199
227
 
@@ -205,6 +233,17 @@ async function resolveTranscriptPath(
205
233
  const candidate = join(claudeProjectsDir, project, `${sessionId}.jsonl`);
206
234
  if (await Bun.file(candidate).exists()) {
207
235
  await Bun.write(cachePath, candidate);
236
+ // Save discovered path to SessionStore for future lookups
237
+ try {
238
+ const { store: writeStore } = openSessionStore(join(projectRoot, ".overstory"));
239
+ try {
240
+ writeStore.updateTranscriptPath(agentName, candidate);
241
+ } finally {
242
+ writeStore.close();
243
+ }
244
+ } catch {
245
+ // Non-fatal: cache write failure should not break transcript resolution
246
+ }
208
247
  return candidate;
209
248
  }
210
249
  }
@@ -773,6 +773,7 @@ describe("mailCommand", () => {
773
773
  lastActivity: new Date().toISOString(),
774
774
  escalationLevel: 0,
775
775
  stalledSince: null,
776
+ transcriptPath: null,
776
777
  },
777
778
  {
778
779
  id: "session-builder-1",
@@ -791,6 +792,7 @@ describe("mailCommand", () => {
791
792
  lastActivity: new Date().toISOString(),
792
793
  escalationLevel: 0,
793
794
  stalledSince: null,
795
+ transcriptPath: null,
794
796
  },
795
797
  {
796
798
  id: "session-builder-2",
@@ -809,6 +811,7 @@ describe("mailCommand", () => {
809
811
  lastActivity: new Date().toISOString(),
810
812
  escalationLevel: 0,
811
813
  stalledSince: null,
814
+ transcriptPath: null,
812
815
  },
813
816
  {
814
817
  id: "session-scout-1",
@@ -827,6 +830,7 @@ describe("mailCommand", () => {
827
830
  lastActivity: new Date().toISOString(),
828
831
  escalationLevel: 0,
829
832
  stalledSince: null,
833
+ transcriptPath: null,
830
834
  },
831
835
  ];
832
836
 
@@ -1147,6 +1151,7 @@ describe("mailCommand", () => {
1147
1151
  lastActivity: new Date().toISOString(),
1148
1152
  escalationLevel: 0,
1149
1153
  stalledSince: null,
1154
+ transcriptPath: null,
1150
1155
  });
1151
1156
  }
1152
1157
 
@@ -16,7 +16,6 @@
16
16
  import { mkdir } from "node:fs/promises";
17
17
  import { join } from "node:path";
18
18
  import { Command } from "commander";
19
- import { deployHooks } from "../agents/hooks-deployer.ts";
20
19
  import { createIdentity, loadIdentity } from "../agents/identity.ts";
21
20
  import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
22
21
  import { loadConfig } from "../config.ts";
@@ -111,8 +110,21 @@ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<v
111
110
  store.updateState(MONITOR_NAME, "completed");
112
111
  }
113
112
 
113
+ // Resolve model and runtime early (needed for deployConfig and spawn)
114
+ const manifestLoader = createManifestLoader(
115
+ join(projectRoot, config.agents.manifestPath),
116
+ join(projectRoot, config.agents.baseDir),
117
+ );
118
+ const manifest = await manifestLoader.load();
119
+ const resolvedModel = resolveModel(config, manifest, "monitor", "sonnet");
120
+ const runtime = getRuntime(undefined, config);
121
+
114
122
  // Deploy monitor-specific hooks to the project root's .claude/ directory.
115
- await deployHooks(projectRoot, MONITOR_NAME, "monitor");
123
+ await runtime.deployConfig(projectRoot, undefined, {
124
+ agentName: MONITOR_NAME,
125
+ capability: "monitor",
126
+ worktreePath: projectRoot,
127
+ });
116
128
 
117
129
  // Create monitor identity if first run
118
130
  const identityBaseDir = join(projectRoot, ".overstory", "agents");
@@ -129,15 +141,6 @@ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<v
129
141
  });
130
142
  }
131
143
 
132
- // Resolve model from config > manifest > fallback
133
- const manifestLoader = createManifestLoader(
134
- join(projectRoot, config.agents.manifestPath),
135
- join(projectRoot, config.agents.baseDir),
136
- );
137
- const manifest = await manifestLoader.load();
138
- const resolvedModel = resolveModel(config, manifest, "monitor", "sonnet");
139
- const runtime = getRuntime(undefined, config);
140
-
141
144
  // Spawn tmux session at project root with Claude Code (interactive mode).
142
145
  const agentDefPath = join(projectRoot, ".overstory", "agent-defs", "monitor.md");
143
146
  const agentDefFile = Bun.file(agentDefPath);
@@ -179,6 +182,7 @@ async function startMonitor(opts: { json: boolean; attach: boolean }): Promise<v
179
182
  lastActivity: new Date().toISOString(),
180
183
  escalationLevel: 0,
181
184
  stalledSince: null,
185
+ transcriptPath: null,
182
186
  };
183
187
 
184
188
  store.upsert(session);
@@ -57,6 +57,7 @@ function makeSession(overrides: Partial<AgentSession> = {}): AgentSession {
57
57
  lastActivity: new Date().toISOString(),
58
58
  escalationLevel: 0,
59
59
  stalledSince: null,
60
+ transcriptPath: null,
60
61
  ...overrides,
61
62
  };
62
63
  }
@@ -167,6 +167,7 @@ recentTasks:
167
167
  lastActivity: new Date().toISOString(),
168
168
  escalationLevel: 0,
169
169
  stalledSince: null,
170
+ transcriptPath: null,
170
171
  },
171
172
  ];
172
173
 
@@ -204,6 +205,7 @@ recentTasks:
204
205
  lastActivity: new Date().toISOString(),
205
206
  escalationLevel: 0,
206
207
  stalledSince: null,
208
+ transcriptPath: null,
207
209
  },
208
210
  ];
209
211
 
@@ -38,6 +38,8 @@ const OVERSTORY_GITIGNORE = `# Wildcard+whitelist: ignore everything, whitelist
38
38
  export interface PrimeOptions {
39
39
  agent?: string;
40
40
  compact?: boolean;
41
+ /** Override the instruction path referenced in agent activation context. Defaults to ".claude/CLAUDE.md". */
42
+ instructionPath?: string;
41
43
  }
42
44
 
43
45
  /**
@@ -138,6 +140,7 @@ async function healGitignore(overstoryDir: string): Promise<void> {
138
140
  export async function primeCommand(opts: PrimeOptions): Promise<void> {
139
141
  const agentName = opts.agent ?? null;
140
142
  const compact = opts.compact ?? false;
143
+ const instructionPath = opts.instructionPath ?? ".claude/CLAUDE.md";
141
144
 
142
145
  // 1. Load config
143
146
  const config = await loadConfig(process.cwd());
@@ -161,7 +164,7 @@ export async function primeCommand(opts: PrimeOptions): Promise<void> {
161
164
  // 4. Output context (orchestrator or agent)
162
165
  if (agentName !== null) {
163
166
  // === Agent priming ===
164
- await outputAgentContext(config, agentName, compact, expertiseOutput);
167
+ await outputAgentContext(config, agentName, compact, expertiseOutput, instructionPath);
165
168
  } else {
166
169
  // === Orchestrator priming ===
167
170
  await outputOrchestratorContext(config, compact, expertiseOutput);
@@ -176,6 +179,7 @@ async function outputAgentContext(
176
179
  agentName: string,
177
180
  compact: boolean,
178
181
  expertiseOutput: string | null,
182
+ instructionPath: string,
179
183
  ): Promise<void> {
180
184
  const sections: string[] = [];
181
185
 
@@ -226,7 +230,7 @@ async function outputAgentContext(
226
230
  if (boundSession) {
227
231
  sections.push("\n## Activation");
228
232
  sections.push(`You have a bound task: **${boundSession.taskId}**`);
229
- sections.push("Read your overlay at `.claude/CLAUDE.md` and begin working immediately.");
233
+ sections.push(`Read your overlay at \`${instructionPath}\` and begin working immediately.`);
230
234
  sections.push("Do not wait for dispatch mail. Your assignment was bound at spawn time.");
231
235
  }
232
236
 
@@ -79,6 +79,7 @@ function makeSession(overrides: Partial<AgentSession> = {}): AgentSession {
79
79
  lastActivity: "2026-02-13T10:30:00.000Z",
80
80
  escalationLevel: 0,
81
81
  stalledSince: null,
82
+ transcriptPath: null,
82
83
  ...overrides,
83
84
  };
84
85
  }
@@ -369,6 +369,7 @@ function makeBeaconOpts(overrides?: Partial<BeaconOptions>): BeaconOptions {
369
369
  taskId: "overstory-abc",
370
370
  parentAgent: null,
371
371
  depth: 0,
372
+ instructionPath: ".claude/CLAUDE.md",
372
373
  ...overrides,
373
374
  };
374
375
  }
@@ -409,12 +410,20 @@ describe("buildBeacon", () => {
409
410
  const opts = makeBeaconOpts({ agentName: "scout-1", taskId: "overstory-xyz" });
410
411
  const beacon = buildBeacon(opts);
411
412
 
412
- expect(beacon).toContain("read .claude/CLAUDE.md");
413
+ expect(beacon).toContain(`read ${opts.instructionPath}`);
413
414
  expect(beacon).toContain("mulch prime");
414
415
  expect(beacon).toContain("ov mail check --agent scout-1");
415
416
  expect(beacon).toContain("begin task overstory-xyz");
416
417
  });
417
418
 
419
+ test("uses custom instructionPath in startup instructions", () => {
420
+ const opts = makeBeaconOpts({ instructionPath: "AGENTS.md" });
421
+ const beacon = buildBeacon(opts);
422
+
423
+ expect(beacon).toContain("read AGENTS.md");
424
+ expect(beacon).not.toContain(".claude/CLAUDE.md");
425
+ });
426
+
418
427
  test("uses agent name in mail check command", () => {
419
428
  const beacon = buildBeacon(makeBeaconOpts({ agentName: "reviewer-beta" }));
420
429
 
@@ -1001,6 +1010,7 @@ function makeAutoDispatchOpts(overrides?: Partial<AutoDispatchOptions>): AutoDis
1001
1010
  capability: "builder",
1002
1011
  specPath: "/path/to/spec.md",
1003
1012
  parentAgent: "lead-alpha",
1013
+ instructionPath: ".claude/CLAUDE.md",
1004
1014
  ...overrides,
1005
1015
  };
1006
1016
  }
@@ -1013,6 +1023,7 @@ describe("buildAutoDispatch", () => {
1013
1023
  capability: "builder",
1014
1024
  specPath: "/path/to/spec.md",
1015
1025
  parentAgent: "lead-alpha",
1026
+ instructionPath: ".claude/CLAUDE.md",
1016
1027
  });
1017
1028
  expect(dispatch.from).toBe("lead-alpha");
1018
1029
  expect(dispatch.to).toBe("builder-1");
@@ -1027,6 +1038,7 @@ describe("buildAutoDispatch", () => {
1027
1038
  capability: "lead",
1028
1039
  specPath: null,
1029
1040
  parentAgent: null,
1041
+ instructionPath: ".claude/CLAUDE.md",
1030
1042
  });
1031
1043
  expect(dispatch.from).toBe("orchestrator");
1032
1044
  expect(dispatch.body).toContain("No spec file");
@@ -1039,6 +1051,7 @@ describe("buildAutoDispatch", () => {
1039
1051
  capability: "scout",
1040
1052
  specPath: null,
1041
1053
  parentAgent: "lead-alpha",
1054
+ instructionPath: ".claude/CLAUDE.md",
1042
1055
  });
1043
1056
  expect(dispatch.body).toContain("scout");
1044
1057
  });
@@ -1050,6 +1063,7 @@ describe("buildAutoDispatch", () => {
1050
1063
  capability: "builder",
1051
1064
  specPath: "/abs/path/to/spec.md",
1052
1065
  parentAgent: "lead-alpha",
1066
+ instructionPath: ".claude/CLAUDE.md",
1053
1067
  });
1054
1068
  expect(dispatch.body).toContain("/abs/path/to/spec.md");
1055
1069
  });
@@ -20,7 +20,6 @@
20
20
 
21
21
  import { mkdir } from "node:fs/promises";
22
22
  import { join, resolve } from "node:path";
23
- import { deployHooks } from "../agents/hooks-deployer.ts";
24
23
  import { createIdentity, loadIdentity } from "../agents/identity.ts";
25
24
  import { createManifestLoader, resolveModel } from "../agents/manifest.ts";
26
25
  import { writeOverlay } from "../agents/overlay.ts";
@@ -132,6 +131,7 @@ export interface AutoDispatchOptions {
132
131
  capability: string;
133
132
  specPath: string | null;
134
133
  parentAgent: string | null;
134
+ instructionPath: string;
135
135
  }
136
136
 
137
137
  /**
@@ -154,7 +154,7 @@ export function buildAutoDispatch(opts: AutoDispatchOptions): {
154
154
  const body = [
155
155
  `You have been assigned task ${opts.taskId} as a ${opts.capability} agent.`,
156
156
  specLine,
157
- `Read your overlay at .claude/CLAUDE.md and begin immediately.`,
157
+ `Read your overlay at ${opts.instructionPath} and begin immediately.`,
158
158
  ].join(" ");
159
159
 
160
160
  return {
@@ -174,6 +174,7 @@ export interface BeaconOptions {
174
174
  taskId: string;
175
175
  parentAgent: string | null;
176
176
  depth: number;
177
+ instructionPath: string;
177
178
  }
178
179
 
179
180
  /**
@@ -198,7 +199,7 @@ export function buildBeacon(opts: BeaconOptions): string {
198
199
  const parts = [
199
200
  `[OVERSTORY] ${opts.agentName} (${opts.capability}) ${timestamp} task:${opts.taskId}`,
200
201
  `Depth: ${opts.depth} | Parent: ${parent}`,
201
- `Startup: read .claude/CLAUDE.md, run mulch prime, check mail (ov mail check --agent ${opts.agentName}), then begin task ${opts.taskId}`,
202
+ `Startup: read ${opts.instructionPath}, run mulch prime, check mail (ov mail check --agent ${opts.agentName}), then begin task ${opts.taskId}`,
202
203
  ];
203
204
  return parts.join(" — ");
204
205
  }
@@ -289,7 +290,7 @@ export function checkParentAgentLimit(
289
290
  *
290
291
  * When parentAgent is null, the caller is the coordinator or a human.
291
292
  * Only "lead" capability is allowed in that case. All other capabilities
292
- * (builder, scout, reviewer, merger) must be spawned by a lead or supervisor
293
+ * (builder, scout, reviewer, merger) must be spawned by a lead
293
294
  * that passes --parent.
294
295
  *
295
296
  * @param parentAgent - The --parent flag value (null = coordinator/human)
@@ -629,8 +630,11 @@ export async function slingCommand(taskId: string, opts: SlingOptions): Promise<
629
630
  trackerName: resolvedBackend,
630
631
  };
631
632
 
633
+ // Resolve runtime before writeOverlay so we can pass runtime.instructionPath
634
+ const runtime = getRuntime(opts.runtime, config);
635
+
632
636
  try {
633
- await writeOverlay(worktreePath, overlayConfig, config.project.root);
637
+ await writeOverlay(worktreePath, overlayConfig, config.project.root, runtime.instructionPath);
634
638
  } catch (err) {
635
639
  // Clean up the orphaned worktree created in step 7 (overstory-p4st)
636
640
  try {
@@ -646,8 +650,16 @@ export async function slingCommand(taskId: string, opts: SlingOptions): Promise<
646
650
  throw err;
647
651
  }
648
652
 
649
- // 9. Deploy hooks config (capability-specific guards)
650
- await deployHooks(worktreePath, name, capability, config.project.qualityGates);
653
+ // 9. Resolve runtime + model (needed for deployConfig, spawn, and beacon)
654
+ const resolvedModel = resolveModel(config, manifest, capability, agentDef.model);
655
+
656
+ // 9a. Deploy hooks config (capability-specific guards)
657
+ await runtime.deployConfig(worktreePath, undefined, {
658
+ agentName: name,
659
+ capability,
660
+ worktreePath,
661
+ qualityGates: config.project.qualityGates,
662
+ });
651
663
 
652
664
  // 9b. Send auto-dispatch mail so it exists when SessionStart hook fires.
653
665
  // This eliminates the race where coordinator sends dispatch AFTER agent boots.
@@ -657,6 +669,7 @@ export async function slingCommand(taskId: string, opts: SlingOptions): Promise<
657
669
  capability,
658
670
  specPath: absoluteSpecPath,
659
671
  parentAgent,
672
+ instructionPath: runtime.instructionPath,
660
673
  });
661
674
  const mailStore = createMailStore(join(overstoryDir, "mail.db"));
662
675
  try {
@@ -701,8 +714,6 @@ export async function slingCommand(taskId: string, opts: SlingOptions): Promise<
701
714
 
702
715
  // 12. Create tmux session running claude in interactive mode
703
716
  const tmuxSessionName = `overstory-${config.project.name}-${name}`;
704
- const resolvedModel = resolveModel(config, manifest, capability, agentDef.model);
705
- const runtime = getRuntime(opts.runtime, config);
706
717
  const spawnCmd = runtime.buildSpawnCommand({
707
718
  model: resolvedModel.model,
708
719
  permissionMode: "bypass",
@@ -740,6 +751,7 @@ export async function slingCommand(taskId: string, opts: SlingOptions): Promise<
740
751
  lastActivity: new Date().toISOString(),
741
752
  escalationLevel: 0,
742
753
  stalledSince: null,
754
+ transcriptPath: null,
743
755
  };
744
756
 
745
757
  store.upsert(session);
@@ -765,6 +777,7 @@ export async function slingCommand(taskId: string, opts: SlingOptions): Promise<
765
777
  taskId,
766
778
  parentAgent,
767
779
  depth,
780
+ instructionPath: runtime.instructionPath,
768
781
  });
769
782
  await sendKeys(tmuxSessionName, beacon);
770
783
 
@@ -780,20 +793,30 @@ export async function slingCommand(taskId: string, opts: SlingOptions): Promise<
780
793
  // screen (detectReady returns "ready"), resend the beacon. Claude Code's TUI
781
794
  // sometimes consumes the Enter keystroke during late initialization, swallowing
782
795
  // the beacon text entirely (overstory-3271).
783
- const verifyAttempts = 5;
784
- for (let v = 0; v < verifyAttempts; v++) {
785
- await Bun.sleep(2_000);
786
- const paneContent = await capturePaneContent(tmuxSessionName);
787
- if (paneContent) {
788
- const readyState = runtime.detectReady(paneContent);
789
- if (readyState.phase !== "ready") {
790
- break; // Agent is processing — beacon was received
796
+ //
797
+ // Skipped for runtimes that return false from requiresBeaconVerification().
798
+ // Pi's TUI idle and processing states are indistinguishable via detectReady
799
+ // (both show "pi v..." header and the token-usage status bar), so the loop
800
+ // would incorrectly conclude the beacon was not received and spam duplicate
801
+ // startup messages.
802
+ const needsVerification =
803
+ !runtime.requiresBeaconVerification || runtime.requiresBeaconVerification();
804
+ if (needsVerification) {
805
+ const verifyAttempts = 5;
806
+ for (let v = 0; v < verifyAttempts; v++) {
807
+ await Bun.sleep(2_000);
808
+ const paneContent = await capturePaneContent(tmuxSessionName);
809
+ if (paneContent) {
810
+ const readyState = runtime.detectReady(paneContent);
811
+ if (readyState.phase !== "ready") {
812
+ break; // Agent is processing — beacon was received
813
+ }
791
814
  }
815
+ // Still at welcome/idle screen — resend beacon
816
+ await sendKeys(tmuxSessionName, beacon);
817
+ await Bun.sleep(1_000);
818
+ await sendKeys(tmuxSessionName, ""); // Follow-up Enter
792
819
  }
793
- // Still at welcome/idle screen — resend beacon
794
- await sendKeys(tmuxSessionName, beacon);
795
- await Bun.sleep(1_000);
796
- await sendKeys(tmuxSessionName, ""); // Follow-up Enter
797
820
  }
798
821
 
799
822
  // 14. Output result
@@ -40,6 +40,7 @@ function makeAgent(overrides: Partial<AgentSession> = {}): AgentSession {
40
40
  lastActivity: new Date().toISOString(),
41
41
  escalationLevel: 0,
42
42
  stalledSince: null,
43
+ transcriptPath: null,
43
44
  ...overrides,
44
45
  };
45
46
  }
@@ -148,6 +148,7 @@ function makeAgentSession(overrides: Partial<AgentSession> = {}): AgentSession {
148
148
  lastActivity: new Date().toISOString(),
149
149
  escalationLevel: 0,
150
150
  stalledSince: null,
151
+ transcriptPath: null,
151
152
  ...overrides,
152
153
  };
153
154
  }
@@ -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,
@@ -551,6 +551,7 @@ describe("traceCommand", () => {
551
551
  lastActivity: new Date().toISOString(),
552
552
  escalationLevel: 0,
553
553
  stalledSince: null,
554
+ transcriptPath: null,
554
555
  });
555
556
  sessionStore.close();
556
557
 
@@ -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
 
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"];