@os-eco/overstory-cli 0.6.1
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/LICENSE +21 -0
- package/README.md +381 -0
- package/agents/builder.md +137 -0
- package/agents/coordinator.md +263 -0
- package/agents/lead.md +301 -0
- package/agents/merger.md +160 -0
- package/agents/monitor.md +214 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +119 -0
- package/agents/supervisor.md +423 -0
- package/package.json +47 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/hooks-deployer.test.ts +2040 -0
- package/src/agents/hooks-deployer.ts +607 -0
- package/src/agents/identity.test.ts +603 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/manifest.test.ts +746 -0
- package/src/agents/manifest.ts +354 -0
- package/src/agents/overlay.test.ts +676 -0
- package/src/agents/overlay.ts +308 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +202 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +322 -0
- package/src/commands/agents.ts +287 -0
- package/src/commands/clean.test.ts +670 -0
- package/src/commands/clean.ts +618 -0
- package/src/commands/completions.test.ts +342 -0
- package/src/commands/completions.ts +887 -0
- package/src/commands/coordinator.test.ts +1530 -0
- package/src/commands/coordinator.ts +733 -0
- package/src/commands/costs.test.ts +1119 -0
- package/src/commands/costs.ts +564 -0
- package/src/commands/dashboard.test.ts +308 -0
- package/src/commands/dashboard.ts +838 -0
- package/src/commands/doctor.test.ts +294 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/errors.test.ts +647 -0
- package/src/commands/errors.ts +248 -0
- package/src/commands/feed.test.ts +578 -0
- package/src/commands/feed.ts +361 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +511 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +253 -0
- package/src/commands/init.test.ts +347 -0
- package/src/commands/init.ts +650 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +431 -0
- package/src/commands/log.test.ts +1454 -0
- package/src/commands/log.ts +724 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +546 -0
- package/src/commands/mail.test.ts +1270 -0
- package/src/commands/mail.ts +771 -0
- package/src/commands/merge.test.ts +670 -0
- package/src/commands/merge.ts +355 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +143 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +390 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +372 -0
- package/src/commands/prime.test.ts +470 -0
- package/src/commands/prime.ts +381 -0
- package/src/commands/replay.test.ts +741 -0
- package/src/commands/replay.ts +360 -0
- package/src/commands/run.test.ts +431 -0
- package/src/commands/run.ts +351 -0
- package/src/commands/sling.test.ts +657 -0
- package/src/commands/sling.ts +661 -0
- package/src/commands/spec.test.ts +203 -0
- package/src/commands/spec.ts +168 -0
- package/src/commands/status.test.ts +430 -0
- package/src/commands/status.ts +398 -0
- package/src/commands/stop.test.ts +420 -0
- package/src/commands/stop.ts +151 -0
- package/src/commands/supervisor.test.ts +187 -0
- package/src/commands/supervisor.ts +535 -0
- package/src/commands/trace.test.ts +745 -0
- package/src/commands/trace.ts +325 -0
- package/src/commands/watch.test.ts +145 -0
- package/src/commands/watch.ts +247 -0
- package/src/commands/worktree.test.ts +786 -0
- package/src/commands/worktree.ts +311 -0
- package/src/config.test.ts +822 -0
- package/src/config.ts +829 -0
- package/src/doctor/agents.test.ts +454 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +190 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +651 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +290 -0
- package/src/doctor/databases.ts +218 -0
- package/src/doctor/dependencies.test.ts +184 -0
- package/src/doctor/dependencies.ts +175 -0
- package/src/doctor/logs.test.ts +251 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +216 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +291 -0
- package/src/doctor/structure.ts +198 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +136 -0
- package/src/doctor/version.ts +129 -0
- package/src/e2e/init-sling-lifecycle.test.ts +277 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +316 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +142 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +813 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +773 -0
- package/src/mail/client.ts +223 -0
- package/src/mail/store.test.ts +705 -0
- package/src/mail/store.ts +387 -0
- package/src/merge/queue.test.ts +359 -0
- package/src/merge/queue.ts +231 -0
- package/src/merge/resolver.test.ts +1345 -0
- package/src/merge/resolver.ts +645 -0
- package/src/metrics/store.test.ts +667 -0
- package/src/metrics/store.ts +445 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +356 -0
- package/src/metrics/transcript.ts +175 -0
- package/src/mulch/client.test.ts +671 -0
- package/src/mulch/client.ts +332 -0
- package/src/sessions/compat.test.ts +280 -0
- package/src/sessions/compat.ts +104 -0
- package/src/sessions/store.test.ts +873 -0
- package/src/sessions/store.ts +494 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +126 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +80 -0
- package/src/tracker/factory.ts +64 -0
- package/src/tracker/seeds.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/types.ts +724 -0
- package/src/watchdog/daemon.test.ts +1975 -0
- package/src/watchdog/daemon.ts +671 -0
- package/src/watchdog/health.test.ts +431 -0
- package/src/watchdog/health.ts +264 -0
- package/src/watchdog/triage.test.ts +164 -0
- package/src/watchdog/triage.ts +179 -0
- package/src/worktree/manager.test.ts +439 -0
- package/src/worktree/manager.ts +198 -0
- package/src/worktree/tmux.test.ts +1009 -0
- package/src/worktree/tmux.ts +509 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +105 -0
- package/templates/overlay.md.tmpl +81 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { AgentError } from "../errors.ts";
|
|
3
|
+
import type {
|
|
4
|
+
AgentDefinition,
|
|
5
|
+
AgentManifest,
|
|
6
|
+
OverstoryConfig,
|
|
7
|
+
ProviderConfig,
|
|
8
|
+
ResolvedModel,
|
|
9
|
+
} from "../types.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Interface for loading, querying, and validating an agent manifest.
|
|
13
|
+
*/
|
|
14
|
+
export interface ManifestLoader {
|
|
15
|
+
/** Load the manifest from disk, parse, validate, and build indexes. */
|
|
16
|
+
load(): Promise<AgentManifest>;
|
|
17
|
+
/** Get an agent definition by name. Returns undefined if not found. */
|
|
18
|
+
getAgent(name: string): AgentDefinition | undefined;
|
|
19
|
+
/** Find all agent names whose capabilities include the given capability. */
|
|
20
|
+
findByCapability(capability: string): AgentDefinition[];
|
|
21
|
+
/** Validate the manifest. Returns a list of errors (empty = valid). */
|
|
22
|
+
validate(): string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Raw manifest shape as read from JSON before validation.
|
|
27
|
+
* Used internally to validate structure before casting to AgentManifest.
|
|
28
|
+
*/
|
|
29
|
+
interface RawManifest {
|
|
30
|
+
version?: unknown;
|
|
31
|
+
agents?: unknown;
|
|
32
|
+
capabilityIndex?: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const MODEL_ALIASES = new Set(["sonnet", "opus", "haiku"]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Validate that a raw parsed object conforms to the AgentDefinition shape.
|
|
39
|
+
* Returns a list of error messages for any violations.
|
|
40
|
+
*/
|
|
41
|
+
function validateAgentDefinition(name: string, raw: unknown): string[] {
|
|
42
|
+
const errors: string[] = [];
|
|
43
|
+
|
|
44
|
+
if (raw === null || typeof raw !== "object") {
|
|
45
|
+
errors.push(`Agent "${name}": definition must be an object`);
|
|
46
|
+
return errors;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const def = raw as Record<string, unknown>;
|
|
50
|
+
|
|
51
|
+
if (typeof def.file !== "string" || def.file.length === 0) {
|
|
52
|
+
errors.push(`Agent "${name}": "file" must be a non-empty string`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof def.model !== "string" || def.model.length === 0) {
|
|
56
|
+
errors.push(`Agent "${name}": "model" must be a non-empty string`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!Array.isArray(def.tools)) {
|
|
60
|
+
errors.push(`Agent "${name}": "tools" must be an array`);
|
|
61
|
+
} else {
|
|
62
|
+
for (let i = 0; i < def.tools.length; i++) {
|
|
63
|
+
if (typeof def.tools[i] !== "string") {
|
|
64
|
+
errors.push(`Agent "${name}": "tools[${i}]" must be a string`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!Array.isArray(def.capabilities)) {
|
|
70
|
+
errors.push(`Agent "${name}": "capabilities" must be an array`);
|
|
71
|
+
} else {
|
|
72
|
+
for (let i = 0; i < def.capabilities.length; i++) {
|
|
73
|
+
if (typeof def.capabilities[i] !== "string") {
|
|
74
|
+
errors.push(`Agent "${name}": "capabilities[${i}]" must be a string`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof def.canSpawn !== "boolean") {
|
|
80
|
+
errors.push(`Agent "${name}": "canSpawn" must be a boolean`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!Array.isArray(def.constraints)) {
|
|
84
|
+
errors.push(`Agent "${name}": "constraints" must be an array`);
|
|
85
|
+
} else {
|
|
86
|
+
for (let i = 0; i < def.constraints.length; i++) {
|
|
87
|
+
if (typeof def.constraints[i] !== "string") {
|
|
88
|
+
errors.push(`Agent "${name}": "constraints[${i}]" must be a string`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return errors;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build a capability index: maps each capability string to the list of
|
|
98
|
+
* agent names that declare that capability.
|
|
99
|
+
*/
|
|
100
|
+
function buildCapabilityIndex(agents: Record<string, AgentDefinition>): Record<string, string[]> {
|
|
101
|
+
const index: Record<string, string[]> = {};
|
|
102
|
+
|
|
103
|
+
for (const [name, def] of Object.entries(agents)) {
|
|
104
|
+
for (const cap of def.capabilities) {
|
|
105
|
+
const existing = index[cap];
|
|
106
|
+
if (existing) {
|
|
107
|
+
existing.push(name);
|
|
108
|
+
} else {
|
|
109
|
+
index[cap] = [name];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return index;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create a ManifestLoader that reads from the given manifest JSON path
|
|
119
|
+
* and resolves agent .md files relative to the given base directory.
|
|
120
|
+
*
|
|
121
|
+
* @param manifestPath - Absolute path to the agent-manifest.json file
|
|
122
|
+
* @param agentBaseDir - Absolute path to the directory containing agent .md files
|
|
123
|
+
*/
|
|
124
|
+
export function createManifestLoader(manifestPath: string, agentBaseDir: string): ManifestLoader {
|
|
125
|
+
let manifest: AgentManifest | null = null;
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
async load(): Promise<AgentManifest> {
|
|
129
|
+
const file = Bun.file(manifestPath);
|
|
130
|
+
const exists = await file.exists();
|
|
131
|
+
|
|
132
|
+
if (!exists) {
|
|
133
|
+
throw new AgentError(`Agent manifest not found: ${manifestPath}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let text: string;
|
|
137
|
+
try {
|
|
138
|
+
text = await file.text();
|
|
139
|
+
} catch (err) {
|
|
140
|
+
throw new AgentError(`Failed to read agent manifest: ${manifestPath}`, {
|
|
141
|
+
cause: err instanceof Error ? err : undefined,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let raw: RawManifest;
|
|
146
|
+
try {
|
|
147
|
+
raw = JSON.parse(text) as RawManifest;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
throw new AgentError(`Failed to parse agent manifest as JSON: ${manifestPath}`, {
|
|
150
|
+
cause: err instanceof Error ? err : undefined,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Validate top-level structure
|
|
155
|
+
if (typeof raw.version !== "string" || raw.version.length === 0) {
|
|
156
|
+
throw new AgentError(
|
|
157
|
+
'Agent manifest missing or invalid "version" field (must be a non-empty string)',
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (raw.agents === null || typeof raw.agents !== "object" || Array.isArray(raw.agents)) {
|
|
162
|
+
throw new AgentError(
|
|
163
|
+
'Agent manifest missing or invalid "agents" field (must be an object)',
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const rawAgents = raw.agents as Record<string, unknown>;
|
|
168
|
+
|
|
169
|
+
// Validate each agent definition
|
|
170
|
+
const allErrors: string[] = [];
|
|
171
|
+
for (const [name, def] of Object.entries(rawAgents)) {
|
|
172
|
+
const defErrors = validateAgentDefinition(name, def);
|
|
173
|
+
allErrors.push(...defErrors);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (allErrors.length > 0) {
|
|
177
|
+
throw new AgentError(`Agent manifest validation failed:\n${allErrors.join("\n")}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// At this point, all agent definitions have been validated
|
|
181
|
+
const agents = rawAgents as Record<string, AgentDefinition>;
|
|
182
|
+
|
|
183
|
+
// Verify that all referenced .md files exist
|
|
184
|
+
for (const [name, def] of Object.entries(agents)) {
|
|
185
|
+
const filePath = join(agentBaseDir, def.file);
|
|
186
|
+
const mdFile = Bun.file(filePath);
|
|
187
|
+
const mdExists = await mdFile.exists();
|
|
188
|
+
|
|
189
|
+
if (!mdExists) {
|
|
190
|
+
throw new AgentError(
|
|
191
|
+
`Agent "${name}" references file "${def.file}" which does not exist at: ${filePath}`,
|
|
192
|
+
{ agentName: name },
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Build the capability index
|
|
198
|
+
const capabilityIndex = buildCapabilityIndex(agents);
|
|
199
|
+
|
|
200
|
+
manifest = {
|
|
201
|
+
version: raw.version,
|
|
202
|
+
agents,
|
|
203
|
+
capabilityIndex,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
return manifest;
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
getAgent(name: string): AgentDefinition | undefined {
|
|
210
|
+
if (!manifest) {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
return manifest.agents[name];
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
findByCapability(capability: string): AgentDefinition[] {
|
|
217
|
+
if (!manifest) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const agentNames = manifest.capabilityIndex[capability];
|
|
222
|
+
if (!agentNames) {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const results: AgentDefinition[] = [];
|
|
227
|
+
for (const name of agentNames) {
|
|
228
|
+
const def = manifest.agents[name];
|
|
229
|
+
if (def) {
|
|
230
|
+
results.push(def);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return results;
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
validate(): string[] {
|
|
237
|
+
if (!manifest) {
|
|
238
|
+
return ["Manifest not loaded. Call load() first."];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const errors: string[] = [];
|
|
242
|
+
|
|
243
|
+
// Re-validate each agent definition structurally
|
|
244
|
+
for (const [name, def] of Object.entries(manifest.agents)) {
|
|
245
|
+
const defErrors = validateAgentDefinition(name, def);
|
|
246
|
+
errors.push(...defErrors);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Verify capability index consistency
|
|
250
|
+
for (const [cap, names] of Object.entries(manifest.capabilityIndex)) {
|
|
251
|
+
for (const name of names) {
|
|
252
|
+
const def = manifest.agents[name];
|
|
253
|
+
if (!def) {
|
|
254
|
+
errors.push(
|
|
255
|
+
`Capability index references agent "${name}" for capability "${cap}", but agent does not exist`,
|
|
256
|
+
);
|
|
257
|
+
} else if (!def.capabilities.includes(cap)) {
|
|
258
|
+
errors.push(
|
|
259
|
+
`Capability index lists agent "${name}" under "${cap}", but agent does not declare that capability`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Check that every agent capability is present in the index
|
|
266
|
+
for (const [name, def] of Object.entries(manifest.agents)) {
|
|
267
|
+
for (const cap of def.capabilities) {
|
|
268
|
+
const indexed = manifest.capabilityIndex[cap];
|
|
269
|
+
if (!indexed || !indexed.includes(name)) {
|
|
270
|
+
errors.push(
|
|
271
|
+
`Agent "${name}" declares capability "${cap}" but is not listed in the capability index`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return errors;
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const DEFAULT_GATEWAY_ALIAS = "sonnet";
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Resolve provider-specific environment variables for a gateway provider.
|
|
286
|
+
*
|
|
287
|
+
* Returns a record of env vars to inject into the tmux session, or null if the
|
|
288
|
+
* provider is not a gateway or lacks required configuration.
|
|
289
|
+
*/
|
|
290
|
+
export function resolveProviderEnv(
|
|
291
|
+
providerName: string,
|
|
292
|
+
modelId: string,
|
|
293
|
+
providers: Record<string, ProviderConfig>,
|
|
294
|
+
env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,
|
|
295
|
+
): Record<string, string> | null {
|
|
296
|
+
const provider = providers[providerName];
|
|
297
|
+
if (!provider || provider.type !== "gateway") return null;
|
|
298
|
+
if (!provider.baseUrl) return null;
|
|
299
|
+
|
|
300
|
+
const alias = DEFAULT_GATEWAY_ALIAS;
|
|
301
|
+
const aliasUpper = alias.toUpperCase();
|
|
302
|
+
|
|
303
|
+
const result: Record<string, string> = {
|
|
304
|
+
ANTHROPIC_BASE_URL: provider.baseUrl,
|
|
305
|
+
ANTHROPIC_API_KEY: "",
|
|
306
|
+
[`ANTHROPIC_DEFAULT_${aliasUpper}_MODEL`]: modelId,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
if (provider.authTokenEnv) {
|
|
310
|
+
const token = env[provider.authTokenEnv];
|
|
311
|
+
if (token) {
|
|
312
|
+
result.ANTHROPIC_AUTH_TOKEN = token;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Resolve the model for an agent role.
|
|
321
|
+
*
|
|
322
|
+
* Resolution order: config.models override > manifest default > fallback.
|
|
323
|
+
*
|
|
324
|
+
* If the model is provider-prefixed (e.g. "openrouter/openai/gpt-5.3") and
|
|
325
|
+
* the named provider is a configured gateway, returns env vars for routing.
|
|
326
|
+
*/
|
|
327
|
+
export function resolveModel(
|
|
328
|
+
config: OverstoryConfig,
|
|
329
|
+
manifest: AgentManifest,
|
|
330
|
+
role: string,
|
|
331
|
+
fallback: string,
|
|
332
|
+
): ResolvedModel {
|
|
333
|
+
const configModel = config.models[role];
|
|
334
|
+
const rawModel = configModel ?? manifest.agents[role]?.model ?? fallback;
|
|
335
|
+
|
|
336
|
+
// Simple alias — no provider env needed
|
|
337
|
+
if (MODEL_ALIASES.has(rawModel)) {
|
|
338
|
+
return { model: rawModel };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Provider-prefixed: split on first "/" to get provider name and model ID
|
|
342
|
+
const slashIdx = rawModel.indexOf("/");
|
|
343
|
+
if (slashIdx > 0) {
|
|
344
|
+
const providerName = rawModel.substring(0, slashIdx);
|
|
345
|
+
const modelId = rawModel.substring(slashIdx + 1);
|
|
346
|
+
const providerEnv = resolveProviderEnv(providerName, modelId, config.providers);
|
|
347
|
+
if (providerEnv) {
|
|
348
|
+
return { model: DEFAULT_GATEWAY_ALIAS, env: providerEnv };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Unknown format — return as-is (may be a direct model string)
|
|
353
|
+
return { model: rawModel };
|
|
354
|
+
}
|