@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.
- package/README.md +6 -5
- package/agents/builder.md +1 -1
- package/agents/coordinator.md +12 -11
- package/agents/lead.md +6 -6
- package/agents/monitor.md +4 -4
- package/agents/reviewer.md +1 -1
- package/agents/scout.md +5 -5
- package/agents/supervisor.md +36 -32
- package/package.json +1 -1
- package/src/agents/guard-rules.ts +97 -0
- package/src/agents/hooks-deployer.ts +7 -90
- package/src/agents/overlay.test.ts +7 -7
- package/src/agents/overlay.ts +5 -5
- package/src/commands/agents.test.ts +5 -0
- package/src/commands/clean.test.ts +3 -0
- package/src/commands/completions.ts +1 -1
- package/src/commands/coordinator.test.ts +1 -0
- package/src/commands/coordinator.ts +15 -11
- package/src/commands/costs.test.ts +5 -0
- package/src/commands/init.test.ts +1 -2
- package/src/commands/init.ts +1 -8
- package/src/commands/inspect.test.ts +14 -0
- package/src/commands/log.test.ts +14 -0
- package/src/commands/log.ts +39 -0
- package/src/commands/mail.test.ts +5 -0
- package/src/commands/monitor.ts +15 -11
- package/src/commands/nudge.test.ts +1 -0
- package/src/commands/prime.test.ts +2 -0
- package/src/commands/prime.ts +6 -2
- package/src/commands/run.test.ts +1 -0
- package/src/commands/sling.test.ts +15 -1
- package/src/commands/sling.ts +44 -21
- package/src/commands/status.test.ts +1 -0
- package/src/commands/stop.test.ts +1 -0
- package/src/commands/supervisor.ts +19 -12
- package/src/commands/trace.test.ts +1 -0
- package/src/commands/worktree.test.ts +9 -0
- package/src/config.ts +29 -0
- package/src/doctor/consistency.test.ts +14 -0
- package/src/e2e/init-sling-lifecycle.test.ts +3 -5
- package/src/index.ts +1 -1
- package/src/mail/broadcast.test.ts +1 -0
- package/src/merge/resolver.ts +23 -4
- package/src/runtimes/claude.test.ts +1 -1
- package/src/runtimes/pi-guards.test.ts +433 -0
- package/src/runtimes/pi-guards.ts +349 -0
- package/src/runtimes/pi.test.ts +620 -0
- package/src/runtimes/pi.ts +244 -0
- package/src/runtimes/registry.test.ts +33 -0
- package/src/runtimes/registry.ts +15 -2
- package/src/runtimes/types.ts +63 -0
- package/src/schema-consistency.test.ts +1 -0
- package/src/sessions/compat.ts +1 -0
- package/src/sessions/store.test.ts +31 -0
- package/src/sessions/store.ts +37 -4
- package/src/types.ts +17 -0
- package/src/watchdog/daemon.test.ts +7 -4
- package/src/watchdog/daemon.ts +1 -1
- package/src/watchdog/health.test.ts +1 -0
- package/src/watchdog/triage.ts +14 -4
package/src/commands/log.ts
CHANGED
|
@@ -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
|
|
package/src/commands/monitor.ts
CHANGED
|
@@ -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
|
|
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);
|
|
@@ -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
|
|
package/src/commands/prime.ts
CHANGED
|
@@ -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(
|
|
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
|
|
package/src/commands/run.test.ts
CHANGED
|
@@ -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(
|
|
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
|
});
|
package/src/commands/sling.ts
CHANGED
|
@@ -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 .
|
|
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 .
|
|
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
|
|
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.
|
|
650
|
-
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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
|
|
@@ -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
|
|
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("
|
|
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,
|
|
@@ -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"];
|