orchestrator-workflow 0.28.0 → 0.30.0
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/CHANGELOG.md +109 -0
- package/INSTALL-AGENT.md +85 -58
- package/README.md +164 -44
- package/assets/agents/implementer.md +64 -2
- package/assets/agents/reviewer.md +48 -1
- package/assets/agents/task-slicer.md +35 -6
- package/assets/agents-md-section.md +25 -4
- package/assets/skill/SKILL.md +189 -28
- package/assets/templates/00-goal.md +38 -0
- package/assets/templates/02-tasks.md +32 -1
- package/assets/templates/03-decisions.md +10 -3
- package/assets/templates/04-implementation-summary.md +44 -0
- package/dist/cli-apply.d.ts +2 -0
- package/dist/cli-inputs.d.ts +13 -17
- package/dist/cli-inputs.js +69 -54
- package/dist/cli.js +110 -6
- package/dist/codex.d.ts +8 -0
- package/dist/codex.js +52 -0
- package/dist/doctor.d.ts +6 -0
- package/dist/doctor.js +29 -5
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/init.d.ts +34 -7
- package/dist/init.js +128 -28
- package/dist/operator-manifest.d.ts +5 -1
- package/dist/operator-manifest.js +33 -1
- package/dist/routing-state.d.ts +60 -0
- package/dist/routing-state.js +277 -0
- package/dist/routing.d.ts +103 -0
- package/dist/routing.js +254 -0
- package/dist/uninstall.js +2 -0
- package/package.json +2 -1
package/dist/init.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Harness } from "./detect.js";
|
|
2
2
|
import type { ModelClass, Profile, Role } from "./models.js";
|
|
3
|
+
import type { HarnessRouting } from "./routing.js";
|
|
4
|
+
import type { OpencodeModelMaps } from "./routing-state.js";
|
|
3
5
|
import type { Report } from "./writers.js";
|
|
4
6
|
export interface InitOptions {
|
|
5
7
|
targetDir: string;
|
|
@@ -15,11 +17,14 @@ export interface InitOptions {
|
|
|
15
17
|
/**
|
|
16
18
|
* Resolved fully-qualified opencode model ids per role, or `undefined` to
|
|
17
19
|
* omit the `model:` frontmatter line (subagent inherits the session model).
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
20
|
+
* Qualified values persist in routing; legacy bare IDs persist in the
|
|
21
|
+
* manifest compatibility map of the same name. A present map records
|
|
22
|
+
* inheritance for missing keys unless routing supplies that selection.
|
|
23
|
+
* When absent, prior
|
|
24
|
+
* selections survive; otherwise `opencodeModelValue(models[role])` passes
|
|
25
|
+
* through a qualified id or leaves bare aliases inheriting the session model.
|
|
21
26
|
*/
|
|
22
|
-
opencodeModels?: Record<Role, string | undefined
|
|
27
|
+
opencodeModels?: Partial<Record<Role, string | undefined>>;
|
|
23
28
|
/**
|
|
24
29
|
* Renders additional per-role effort-tier subagent variants
|
|
25
30
|
* (`<role>-<tier>.md`) alongside the default agent file. Defaults to
|
|
@@ -33,9 +38,29 @@ export interface InitOptions {
|
|
|
33
38
|
* that variant. Only consulted when `tiers` is true and the `opencode`
|
|
34
39
|
* harness is selected; mirrors `opencodeModels` but keyed by model class
|
|
35
40
|
* instead of role, since a tier variant's model is chosen by class, not
|
|
36
|
-
* by the role's own preselected model.
|
|
41
|
+
* by the role's own preselected model. Resolved ids are persisted per tier
|
|
42
|
+
* and retained on a later install that omits this field. Legacy bare IDs
|
|
43
|
+
* persist in the manifest compatibility map of the same name.
|
|
37
44
|
*/
|
|
38
|
-
opencodeClassModels?: Record<ModelClass, string | undefined
|
|
45
|
+
opencodeClassModels?: Partial<Record<ModelClass, string | undefined>>;
|
|
46
|
+
/**
|
|
47
|
+
* Per-harness role/tier selections. This is an additive, partial map: the
|
|
48
|
+
* installer preserves previous leaves and fills remaining omissions from
|
|
49
|
+
* deterministic harness defaults. An explicit leaf wins over legacy inputs.
|
|
50
|
+
*/
|
|
51
|
+
routing?: HarnessRouting;
|
|
52
|
+
/**
|
|
53
|
+
* "patch" (default) preserves previous routing leaves. "replace" accepts
|
|
54
|
+
* an authoritative resolved map and excludes all previous routing, as used
|
|
55
|
+
* by CLI resolution and apply --sync. Omitted leaves still use defaults.
|
|
56
|
+
*/
|
|
57
|
+
routingMode?: "patch" | "replace";
|
|
58
|
+
/**
|
|
59
|
+
* Optional, explicitly supplied Codex capability catalog. The installer
|
|
60
|
+
* never contacts a live service; when this is present it is validated
|
|
61
|
+
* before any kit-owned file is written.
|
|
62
|
+
*/
|
|
63
|
+
codexCatalog?: unknown;
|
|
39
64
|
/**
|
|
40
65
|
* Repo kit-version pin (distinct from the actually-installed `version`),
|
|
41
66
|
* so a later `apply` command can gate on it. A `string` sets a new
|
|
@@ -47,7 +72,7 @@ export interface InitOptions {
|
|
|
47
72
|
pin?: string | null;
|
|
48
73
|
}
|
|
49
74
|
export declare const MANIFEST_PATH: string;
|
|
50
|
-
export interface Manifest {
|
|
75
|
+
export interface Manifest extends OpencodeModelMaps {
|
|
51
76
|
kit: string;
|
|
52
77
|
version: string;
|
|
53
78
|
harnesses: Harness[];
|
|
@@ -56,6 +81,8 @@ export interface Manifest {
|
|
|
56
81
|
profile: Profile;
|
|
57
82
|
/** Whether per-role effort-tier subagent variants were rendered. */
|
|
58
83
|
tiers: boolean;
|
|
84
|
+
/** Exact resolved harness/role/tier selections from the install. */
|
|
85
|
+
routing?: HarnessRouting;
|
|
59
86
|
/**
|
|
60
87
|
* sha256 of every kit-owned file as installed. This is how a re-run tells
|
|
61
88
|
* "upstream changed, safe to update" apart from "user edited, conflict".
|
package/dist/init.js
CHANGED
|
@@ -4,6 +4,9 @@ import { isAbsolute, join, normalize, sep } from "node:path";
|
|
|
4
4
|
import { PACKAGE_VERSION, listTemplateNames, readAgentAsset, readAsset, } from "./assets.js";
|
|
5
5
|
import { HARNESSES } from "./detect.js";
|
|
6
6
|
import { CLASS_MODELS, DEFAULT_PROFILE, DEFAULT_TIER, READ_ONLY_ROLES, ROLES, ROLE_TIERS, TIER_DEFS, assertValidModelId, claudeModelValue, isProfile, opencodeModelValue, rolesForProfile, } from "./models.js";
|
|
7
|
+
import { composeCodexAgent } from "./codex.js";
|
|
8
|
+
import { parseRouting } from "./routing.js";
|
|
9
|
+
import { codexCatalogWarnings, normalizeRoutingState, legacyOpencodeFallbacks, parseOpencodeModelMaps, } from "./routing-state.js";
|
|
7
10
|
import { emptyReport, ensureClaudeImport, installFile, upsertMarkerSection, } from "./writers.js";
|
|
8
11
|
const SKILL_NAME = "orchestrator-workflow";
|
|
9
12
|
export const MANIFEST_PATH = join(".ai", "workflow", "manifest.json"); // shared with doctor.ts/cli.ts (L9); see readInstalledManifest below
|
|
@@ -91,6 +94,14 @@ export function readInstalledManifest(targetDir) {
|
|
|
91
94
|
// (the same per-field-degradation style as `profile` above) rather than
|
|
92
95
|
// throwing on a legacy manifest.
|
|
93
96
|
const tiers = typeof candidate.tiers === "boolean" ? candidate.tiers : false;
|
|
97
|
+
// Routing is intentionally stricter than the legacy per-role `models`
|
|
98
|
+
// map. A malformed routing record must stop a re-install instead of
|
|
99
|
+
// silently replacing the operator's explicit selections with new defaults.
|
|
100
|
+
const opencodeMaps = parseOpencodeModelMaps(candidate);
|
|
101
|
+
let routing;
|
|
102
|
+
if ("routing" in candidate) {
|
|
103
|
+
routing = parseRouting(candidate.routing);
|
|
104
|
+
}
|
|
94
105
|
// A hand-written or damaged manifest may carry a non-string `pin`; that
|
|
95
106
|
// degrades to "no recorded pin" here (the same per-field-degradation
|
|
96
107
|
// style as `profile`/`tiers` above) rather than throwing. An empty or
|
|
@@ -105,6 +116,8 @@ export function readInstalledManifest(targetDir) {
|
|
|
105
116
|
models: models,
|
|
106
117
|
profile,
|
|
107
118
|
tiers,
|
|
119
|
+
...(routing !== undefined ? { routing } : {}),
|
|
120
|
+
...opencodeMaps,
|
|
108
121
|
files,
|
|
109
122
|
installedAt: typeof candidate.installedAt === "string" ? candidate.installedAt : "",
|
|
110
123
|
// The kit-version pin is deliberately free-form here: unlike the
|
|
@@ -118,6 +131,17 @@ export function readInstalledManifest(targetDir) {
|
|
|
118
131
|
function yamlQuote(value) {
|
|
119
132
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
120
133
|
}
|
|
134
|
+
function yamlModelScalar(value) {
|
|
135
|
+
// Keep ordinary legacy aliases and provider ids byte-compatible, but quote
|
|
136
|
+
// values YAML would otherwise coerce when they originate in routing JSON.
|
|
137
|
+
if (/^(?:true|false|null|yes|no|on|off|~)$/i.test(value) ||
|
|
138
|
+
/^[-+]?\d+(?:\.\d+)?$/.test(value) ||
|
|
139
|
+
value.includes(":") ||
|
|
140
|
+
value.includes("#")) {
|
|
141
|
+
return yamlQuote(value);
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
121
145
|
/**
|
|
122
146
|
* Composes the unsuffixed default agent file. It carries a pinned
|
|
123
147
|
* `effort: <TIER_DEFS[DEFAULT_TIER[role]].effort>` line unconditionally
|
|
@@ -134,14 +158,14 @@ function yamlQuote(value) {
|
|
|
134
158
|
* opencode's family-based `opencodeEffortLine` has, so there is no second,
|
|
135
159
|
* potentially-diverging computation here to guard against.
|
|
136
160
|
*/
|
|
137
|
-
function composeClaudeAgent(role, model) {
|
|
161
|
+
function composeClaudeAgent(role, model, selection) {
|
|
138
162
|
const asset = readAgentAsset(role);
|
|
139
163
|
const frontmatter = [
|
|
140
164
|
"---",
|
|
141
165
|
`name: ${asset.name}`,
|
|
142
166
|
`description: ${yamlQuote(asset.description)}`,
|
|
143
|
-
`model: ${claudeModelValue(model)}`,
|
|
144
|
-
`effort: ${TIER_DEFS[DEFAULT_TIER[role]].effort}`,
|
|
167
|
+
`model: ${selection ? yamlModelScalar(selection.model) : claudeModelValue(model)}`,
|
|
168
|
+
`effort: ${selection?.effort ?? TIER_DEFS[DEFAULT_TIER[role]].effort}`,
|
|
145
169
|
];
|
|
146
170
|
// Read-only roles keep every read/search tool but cannot mutate files.
|
|
147
171
|
if (READ_ONLY_ROLES.has(role)) {
|
|
@@ -168,7 +192,7 @@ function composeOpencodeAgent(role, modelValue, effortLine) {
|
|
|
168
192
|
// Only emit `model:` when a resolved, non-empty FQ id is available.
|
|
169
193
|
// Omitting it lets the subagent inherit the session/default model.
|
|
170
194
|
if (modelValue) {
|
|
171
|
-
frontmatter.push(`model: ${modelValue}`);
|
|
195
|
+
frontmatter.push(`model: ${yamlModelScalar(modelValue)}`);
|
|
172
196
|
}
|
|
173
197
|
if (effortLine) {
|
|
174
198
|
frontmatter.push(effortLine);
|
|
@@ -179,6 +203,9 @@ function composeOpencodeAgent(role, modelValue, effortLine) {
|
|
|
179
203
|
frontmatter.push("---");
|
|
180
204
|
return [...frontmatter, "", asset.body.trimEnd(), ""].join("\n");
|
|
181
205
|
}
|
|
206
|
+
function routingSelection(routing, harness, role, tier) {
|
|
207
|
+
return routing[harness]?.[role]?.[tier];
|
|
208
|
+
}
|
|
182
209
|
function tierDescriptionSuffix(asset, tier) {
|
|
183
210
|
return `${asset.description} (Effort tier: ${tier}.)`;
|
|
184
211
|
}
|
|
@@ -188,15 +215,15 @@ function tierDescriptionSuffix(asset, tier) {
|
|
|
188
215
|
* `DEFAULT_TIER`), so `composeClaudeAgent`'s own output (pinned default
|
|
189
216
|
* effort included) stays byte-identical whether or not tiers are on.
|
|
190
217
|
*/
|
|
191
|
-
function composeClaudeAgentVariant(role, tier) {
|
|
218
|
+
function composeClaudeAgentVariant(role, tier, selection) {
|
|
192
219
|
const asset = readAgentAsset(role);
|
|
193
220
|
const def = TIER_DEFS[tier];
|
|
194
221
|
const frontmatter = [
|
|
195
222
|
"---",
|
|
196
223
|
`name: ${asset.name}-${tier}`,
|
|
197
224
|
`description: ${yamlQuote(tierDescriptionSuffix(asset, tier))}`,
|
|
198
|
-
`model: ${claudeModelValue(CLASS_MODELS[def.modelClass])}`,
|
|
199
|
-
`effort: ${def.effort}`,
|
|
225
|
+
`model: ${selection ? yamlModelScalar(selection.model) : claudeModelValue(CLASS_MODELS[def.modelClass])}`,
|
|
226
|
+
`effort: ${selection?.effort ?? def.effort}`,
|
|
200
227
|
];
|
|
201
228
|
if (READ_ONLY_ROLES.has(role)) {
|
|
202
229
|
frontmatter.push("disallowedTools: Edit, Write, NotebookEdit");
|
|
@@ -264,7 +291,7 @@ function composeOpencodeAgentVariant(role, tier, modelValue, effortLine) {
|
|
|
264
291
|
"mode: subagent",
|
|
265
292
|
];
|
|
266
293
|
if (modelValue) {
|
|
267
|
-
frontmatter.push(`model: ${modelValue}`);
|
|
294
|
+
frontmatter.push(`model: ${yamlModelScalar(modelValue)}`);
|
|
268
295
|
}
|
|
269
296
|
if (effortLine) {
|
|
270
297
|
frontmatter.push(effortLine);
|
|
@@ -288,6 +315,43 @@ export function runInit(options) {
|
|
|
288
315
|
const tiers = options.tiers ?? false;
|
|
289
316
|
const report = emptyReport();
|
|
290
317
|
const previous = readInstalledManifest(targetDir);
|
|
318
|
+
const suppliedMaps = parseOpencodeModelMaps(options);
|
|
319
|
+
const previousMaps = options.routingMode === "replace"
|
|
320
|
+
? {}
|
|
321
|
+
: parseOpencodeModelMaps(previous ?? {});
|
|
322
|
+
const opencodeModels = suppliedMaps.opencodeModels ??
|
|
323
|
+
(previousMaps.opencodeModels !== undefined
|
|
324
|
+
? { ...previousMaps.opencodeModels }
|
|
325
|
+
: undefined);
|
|
326
|
+
if (opencodeModels && options.opencodeModels === undefined && previous) {
|
|
327
|
+
for (const role of ROLES) {
|
|
328
|
+
if (options.models[role] !== previous.models[role])
|
|
329
|
+
opencodeModels[role] = opencodeModelValue(options.models[role]);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const opencodeClassModels = suppliedMaps.opencodeClassModels ?? previousMaps.opencodeClassModels;
|
|
333
|
+
const compatibility = legacyOpencodeFallbacks({
|
|
334
|
+
opencodeModels,
|
|
335
|
+
opencodeClassModels,
|
|
336
|
+
});
|
|
337
|
+
const routing = normalizeRoutingState({
|
|
338
|
+
harnesses: options.harnesses,
|
|
339
|
+
models: options.models,
|
|
340
|
+
opencodeModels,
|
|
341
|
+
opencodeClassModels,
|
|
342
|
+
previousRouting: previous?.routing,
|
|
343
|
+
routing: options.routing,
|
|
344
|
+
routingMode: options.routingMode,
|
|
345
|
+
legacyOverrideRoles: previous
|
|
346
|
+
? ROLES.filter((role) => options.models[role] !== previous.models[role])
|
|
347
|
+
: [],
|
|
348
|
+
updateOpencodeModels: options.opencodeModels !== undefined,
|
|
349
|
+
updateOpencodeClassModels: options.opencodeClassModels !== undefined,
|
|
350
|
+
});
|
|
351
|
+
// Check only rendered selections before the first mutation.
|
|
352
|
+
for (const warning of codexCatalogWarnings(routing, { harnesses: options.harnesses, profile, tiers }, options.codexCatalog)) {
|
|
353
|
+
report.notes.push(`Codex catalog: ${warning}`);
|
|
354
|
+
}
|
|
291
355
|
// `null` clears an existing pin, a string sets a new one, and omitted
|
|
292
356
|
// (`undefined`) carries the previous manifest's pin forward unchanged. An
|
|
293
357
|
// empty or whitespace-only string is normalized to a clear as well: it can
|
|
@@ -315,7 +379,15 @@ export function runInit(options) {
|
|
|
315
379
|
// sitting on disk and still becoming untracked either way. The ledger
|
|
316
380
|
// (`previous.files`/`previous.harnesses`) is the only source of truth for
|
|
317
381
|
// "what did the previous install actually put on disk."
|
|
318
|
-
const previousHarnessDirs = (previous?.harnesses ?? [])
|
|
382
|
+
const previousHarnessDirs = (previous?.harnesses ?? [])
|
|
383
|
+
.map((harness) => harness === "claude"
|
|
384
|
+
? { dir: ".claude", extension: ".md" }
|
|
385
|
+
: harness === "opencode"
|
|
386
|
+
? { dir: ".opencode", extension: ".md" }
|
|
387
|
+
: harness === "codex"
|
|
388
|
+
? { dir: ".codex", extension: ".toml" }
|
|
389
|
+
: undefined)
|
|
390
|
+
.filter((entry) => entry !== undefined);
|
|
319
391
|
// A full -> minimal downgrade drops explorer/task-slicer from the roles
|
|
320
392
|
// installed, but (like dropping a harness from --harness) existing role
|
|
321
393
|
// files are never deleted: they simply fall out of the manifest's file
|
|
@@ -323,9 +395,9 @@ export function runInit(options) {
|
|
|
323
395
|
// left as an unexplained, untracked leftover on disk.
|
|
324
396
|
if (previous && previous.profile === "full" && profile !== previous.profile) {
|
|
325
397
|
const droppedRoles = rolesForProfile(previous.profile).filter((role) => !rolesForProfile(profile).includes(role));
|
|
326
|
-
for (const harnessDir of previousHarnessDirs
|
|
398
|
+
for (const harnessDir of previousHarnessDirs) {
|
|
327
399
|
for (const role of droppedRoles) {
|
|
328
|
-
const relativePath = join(harnessDir, "agents", `${role}.
|
|
400
|
+
const relativePath = join(harnessDir.dir, "agents", `${role}${harnessDir.extension}`);
|
|
329
401
|
if (previous.files[relativePath] !== undefined) {
|
|
330
402
|
report.notes.push(`${relativePath}: now untracked after the full -> ${profile} profile downgrade; run \`orchestrator-workflow uninstall\` first next time, or remove it by hand.`);
|
|
331
403
|
}
|
|
@@ -338,7 +410,7 @@ export function runInit(options) {
|
|
|
338
410
|
for (const tier of ROLE_TIERS[role]) {
|
|
339
411
|
if (tier === DEFAULT_TIER[role])
|
|
340
412
|
continue;
|
|
341
|
-
const variantPath = join(harnessDir, "agents", `${role}-${tier}.
|
|
413
|
+
const variantPath = join(harnessDir.dir, "agents", `${role}-${tier}${harnessDir.extension}`);
|
|
342
414
|
if (previous.files[variantPath] !== undefined) {
|
|
343
415
|
report.notes.push(`${variantPath}: now untracked after the full -> ${profile} profile downgrade; run \`orchestrator-workflow uninstall\` first next time, or remove it by hand.`);
|
|
344
416
|
}
|
|
@@ -353,12 +425,12 @@ export function runInit(options) {
|
|
|
353
425
|
// there is no overlap between the two loops). Surface it the same way:
|
|
354
426
|
// a note per file instead of a silent, unexplained leftover.
|
|
355
427
|
if (previous && previous.tiers && !tiers) {
|
|
356
|
-
for (const harnessDir of previousHarnessDirs
|
|
428
|
+
for (const harnessDir of previousHarnessDirs) {
|
|
357
429
|
for (const role of rolesForProfile(profile)) {
|
|
358
430
|
for (const tier of ROLE_TIERS[role]) {
|
|
359
431
|
if (tier === DEFAULT_TIER[role])
|
|
360
432
|
continue;
|
|
361
|
-
const relativePath = join(harnessDir, "agents", `${role}-${tier}.
|
|
433
|
+
const relativePath = join(harnessDir.dir, "agents", `${role}-${tier}${harnessDir.extension}`);
|
|
362
434
|
if (previous.files[relativePath] !== undefined) {
|
|
363
435
|
report.notes.push(`${relativePath}: now untracked after tiers were turned off; run \`orchestrator-workflow uninstall\` first next time, or remove it by hand.`);
|
|
364
436
|
}
|
|
@@ -390,16 +462,15 @@ export function runInit(options) {
|
|
|
390
462
|
// either way, whether or not `previous.harnesses` ever named it.
|
|
391
463
|
if (previous) {
|
|
392
464
|
const harnessDirs = {
|
|
393
|
-
claude: ".claude",
|
|
394
|
-
codex: ".agents",
|
|
395
|
-
opencode: ".opencode",
|
|
465
|
+
claude: [".claude"],
|
|
466
|
+
codex: [".agents", ".codex"],
|
|
467
|
+
opencode: [".opencode"],
|
|
396
468
|
};
|
|
397
469
|
for (const harness of HARNESSES) {
|
|
398
470
|
if (options.harnesses.includes(harness))
|
|
399
471
|
continue;
|
|
400
|
-
const prefix = harnessDirs[harness] + sep;
|
|
401
472
|
for (const relativePath of Object.keys(previous.files)) {
|
|
402
|
-
if (relativePath.startsWith(
|
|
473
|
+
if (harnessDirs[harness].some((dir) => relativePath.startsWith(dir + sep))) {
|
|
403
474
|
report.notes.push(`${relativePath}: now untracked after --harness dropped ${harness}; run \`orchestrator-workflow uninstall\` first next time, or remove it by hand.`);
|
|
404
475
|
}
|
|
405
476
|
}
|
|
@@ -415,7 +486,7 @@ export function runInit(options) {
|
|
|
415
486
|
// `previous.files`), not on the sanitized `previous.harnesses`, for the
|
|
416
487
|
// same reason as the loop above: a damaged manifest can filter a valid
|
|
417
488
|
// harness out of `previous.harnesses` while its files remain recorded.
|
|
418
|
-
const hadTrackedHarnessFiles = Object.keys(previous.files).some((relativePath) => HARNESSES.some((harness) =>
|
|
489
|
+
const hadTrackedHarnessFiles = Object.keys(previous.files).some((relativePath) => HARNESSES.some((harness) => harnessDirs[harness].some((dir) => relativePath.startsWith(dir + sep))));
|
|
419
490
|
if (hadTrackedHarnessFiles && options.harnesses.length === 0) {
|
|
420
491
|
for (const name of ["AGENTS.md", "CLAUDE.md"]) {
|
|
421
492
|
if (existsSync(join(targetDir, name))) {
|
|
@@ -466,12 +537,12 @@ export function runInit(options) {
|
|
|
466
537
|
if (options.harnesses.includes("claude")) {
|
|
467
538
|
installKitFile(join(".claude", "skills", SKILL_NAME, "SKILL.md"), skill);
|
|
468
539
|
for (const role of rolesForProfile(profile)) {
|
|
469
|
-
installKitFile(join(".claude", "agents", `${role}.md`), composeClaudeAgent(role, options.models[role]));
|
|
540
|
+
installKitFile(join(".claude", "agents", `${role}.md`), composeClaudeAgent(role, options.models[role], routingSelection(routing, "claude", role, DEFAULT_TIER[role])));
|
|
470
541
|
if (tiers) {
|
|
471
542
|
for (const tier of ROLE_TIERS[role]) {
|
|
472
543
|
if (tier === DEFAULT_TIER[role])
|
|
473
544
|
continue;
|
|
474
|
-
installKitFile(join(".claude", "agents", `${role}-${tier}.md`), composeClaudeAgentVariant(role, tier));
|
|
545
|
+
installKitFile(join(".claude", "agents", `${role}-${tier}.md`), composeClaudeAgentVariant(role, tier, routingSelection(routing, "claude", role, tier)));
|
|
475
546
|
}
|
|
476
547
|
}
|
|
477
548
|
}
|
|
@@ -479,21 +550,46 @@ export function runInit(options) {
|
|
|
479
550
|
}
|
|
480
551
|
if (options.harnesses.includes("codex")) {
|
|
481
552
|
installKitFile(join(".agents", "skills", SKILL_NAME, "SKILL.md"), skill);
|
|
553
|
+
for (const role of rolesForProfile(profile)) {
|
|
554
|
+
const defaultSelection = routingSelection(routing, "codex", role, DEFAULT_TIER[role]);
|
|
555
|
+
// `defaultCodexRouting` makes every default-tier selection complete;
|
|
556
|
+
// retain this guard so a hand-constructed partial routing object cannot
|
|
557
|
+
// produce an invalid native agent file through the public runInit API.
|
|
558
|
+
if (!defaultSelection) {
|
|
559
|
+
throw new Error(`Missing Codex routing for ${role}/${DEFAULT_TIER[role]}`);
|
|
560
|
+
}
|
|
561
|
+
installKitFile(join(".codex", "agents", `${role}.toml`), composeCodexAgent(role, defaultSelection));
|
|
562
|
+
if (tiers) {
|
|
563
|
+
for (const tier of ROLE_TIERS[role]) {
|
|
564
|
+
if (tier === DEFAULT_TIER[role])
|
|
565
|
+
continue;
|
|
566
|
+
const selection = routingSelection(routing, "codex", role, tier);
|
|
567
|
+
if (!selection) {
|
|
568
|
+
throw new Error(`Missing Codex routing for ${role}/${tier}`);
|
|
569
|
+
}
|
|
570
|
+
installKitFile(join(".codex", "agents", `${role}-${tier}.toml`), composeCodexAgent(role, selection, tier));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
482
574
|
}
|
|
483
575
|
if (options.harnesses.includes("opencode")) {
|
|
484
576
|
installKitFile(join(".opencode", "skills", SKILL_NAME, "SKILL.md"), skill);
|
|
485
577
|
for (const role of rolesForProfile(profile)) {
|
|
486
|
-
const modelValue =
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
578
|
+
const modelValue = routingSelection(routing, "opencode", role, DEFAULT_TIER[role])
|
|
579
|
+
?.model ??
|
|
580
|
+
(opencodeModels !== undefined
|
|
581
|
+
? opencodeModels[role]
|
|
582
|
+
: opencodeModelValue(options.models[role]));
|
|
583
|
+
const defaultEffortLine = opencodeEffortLine(routingSelection(routing, "opencode", role, DEFAULT_TIER[role])
|
|
584
|
+
?.effort ?? DEFAULT_TIER[role], modelValue);
|
|
490
585
|
installKitFile(join(".opencode", "agents", `${role}.md`), composeOpencodeAgent(role, modelValue, defaultEffortLine));
|
|
491
586
|
if (tiers) {
|
|
492
587
|
for (const tier of ROLE_TIERS[role]) {
|
|
493
588
|
if (tier === DEFAULT_TIER[role])
|
|
494
589
|
continue;
|
|
590
|
+
const selection = routingSelection(routing, "opencode", role, tier);
|
|
495
591
|
const modelClass = TIER_DEFS[tier].modelClass;
|
|
496
|
-
const variantModelValue =
|
|
592
|
+
const variantModelValue = selection?.model ?? opencodeClassModels?.[modelClass];
|
|
497
593
|
if (variantModelValue === undefined) {
|
|
498
594
|
// No model resolved for this class: opencodeEffortLine always
|
|
499
595
|
// returns undefined too when its modelValue argument is
|
|
@@ -509,7 +605,7 @@ export function runInit(options) {
|
|
|
509
605
|
// rules in opencodeEffortLine above.
|
|
510
606
|
continue;
|
|
511
607
|
}
|
|
512
|
-
const effortLine = opencodeEffortLine(tier, variantModelValue);
|
|
608
|
+
const effortLine = opencodeEffortLine(selection?.effort ?? tier, variantModelValue);
|
|
513
609
|
installKitFile(join(".opencode", "agents", `${role}-${tier}.md`), composeOpencodeAgentVariant(role, tier, variantModelValue, effortLine));
|
|
514
610
|
}
|
|
515
611
|
}
|
|
@@ -524,6 +620,8 @@ export function runInit(options) {
|
|
|
524
620
|
models: options.models,
|
|
525
621
|
profile,
|
|
526
622
|
tiers,
|
|
623
|
+
routing,
|
|
624
|
+
...compatibility,
|
|
527
625
|
files: installedFiles,
|
|
528
626
|
...(pin !== undefined ? { pin } : {}),
|
|
529
627
|
};
|
|
@@ -536,6 +634,8 @@ export function runInit(options) {
|
|
|
536
634
|
models: previous.models,
|
|
537
635
|
profile: previous.profile,
|
|
538
636
|
tiers: previous.tiers,
|
|
637
|
+
...(previous.routing !== undefined ? { routing: previous.routing } : {}),
|
|
638
|
+
...legacyOpencodeFallbacks(previous),
|
|
539
639
|
files: previous.files,
|
|
540
640
|
...(previous.pin !== undefined ? { pin: previous.pin } : {}),
|
|
541
641
|
}) === JSON.stringify(desired)) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Harness } from "./detect.js";
|
|
2
2
|
import type { Profile, Role } from "./models.js";
|
|
3
|
+
import type { OpencodeModelMaps } from "./routing-state.js";
|
|
4
|
+
import type { HarnessRouting } from "./routing.js";
|
|
3
5
|
export declare const OPERATOR_HOME_DIRNAME = ".orchestrator-workflow";
|
|
4
6
|
export declare const OPERATOR_HOME_ENV = "ORCHESTRATOR_WORKFLOW_HOME";
|
|
5
7
|
export declare const OPERATOR_MANIFEST_FILENAME = "manifest.json";
|
|
@@ -11,11 +13,13 @@ export declare const OPERATOR_MANIFEST_FILENAME = "manifest.json";
|
|
|
11
13
|
* some roles, and the rest should fall back to `DEFAULT_MODELS` at the call
|
|
12
14
|
* site rather than forcing every role to be present here.
|
|
13
15
|
*/
|
|
14
|
-
export interface OperatorManifestDefaults {
|
|
16
|
+
export interface OperatorManifestDefaults extends OpencodeModelMaps {
|
|
15
17
|
harnesses: Harness[];
|
|
16
18
|
profile: Profile;
|
|
17
19
|
tiers: boolean;
|
|
18
20
|
models: Partial<Record<Role, string>>;
|
|
21
|
+
/** Explicit or resolved per-harness selections, carried additively. */
|
|
22
|
+
routing?: HarnessRouting;
|
|
19
23
|
}
|
|
20
24
|
/** One target directory this operator has applied the kit to. */
|
|
21
25
|
export interface OperatorTarget {
|
|
@@ -4,6 +4,8 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { HARNESSES } from "./detect.js";
|
|
6
6
|
import { DEFAULT_PROFILE, ROLES, assertValidModelId, isProfile, } from "./models.js";
|
|
7
|
+
import { parseRouting } from "./routing.js";
|
|
8
|
+
import { parseOpencodeModelMaps } from "./routing-state.js";
|
|
7
9
|
export const OPERATOR_HOME_DIRNAME = ".orchestrator-workflow";
|
|
8
10
|
export const OPERATOR_HOME_ENV = "ORCHESTRATOR_WORKFLOW_HOME";
|
|
9
11
|
export const OPERATOR_MANIFEST_FILENAME = "manifest.json";
|
|
@@ -88,6 +90,25 @@ export function readOperatorManifest(home) {
|
|
|
88
90
|
? rawDefaults.profile
|
|
89
91
|
: DEFAULT_PROFILE;
|
|
90
92
|
const tiers = typeof rawDefaults.tiers === "boolean" ? rawDefaults.tiers : false;
|
|
93
|
+
let opencodeMaps;
|
|
94
|
+
try {
|
|
95
|
+
opencodeMaps = parseOpencodeModelMaps(rawDefaults);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
let routing;
|
|
101
|
+
if ("routing" in rawDefaults) {
|
|
102
|
+
try {
|
|
103
|
+
routing = parseRouting(rawDefaults.routing);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Unlike legacy aliases, an invalid routing map could silently change
|
|
107
|
+
// native Codex agents on a future install. Treat the whole operator
|
|
108
|
+
// manifest as unreadable so setup/apply cannot overwrite it by guess.
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
91
112
|
const targets = (Array.isArray(candidate.targets) ? candidate.targets : []).filter((value) => {
|
|
92
113
|
if (typeof value !== "object" || value === null)
|
|
93
114
|
return false;
|
|
@@ -100,7 +121,14 @@ export function readOperatorManifest(home) {
|
|
|
100
121
|
return {
|
|
101
122
|
kit: "orchestrator-workflow",
|
|
102
123
|
schemaVersion: 1,
|
|
103
|
-
defaults: {
|
|
124
|
+
defaults: {
|
|
125
|
+
harnesses,
|
|
126
|
+
profile,
|
|
127
|
+
tiers,
|
|
128
|
+
models,
|
|
129
|
+
...(routing !== undefined ? { routing } : {}),
|
|
130
|
+
...opencodeMaps,
|
|
131
|
+
},
|
|
104
132
|
targets,
|
|
105
133
|
createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : "",
|
|
106
134
|
updatedAt: typeof candidate.updatedAt === "string" ? candidate.updatedAt : "",
|
|
@@ -135,6 +163,7 @@ function writeOperatorManifestUnlocked(home, manifest) {
|
|
|
135
163
|
mkdirSync(home, { recursive: true });
|
|
136
164
|
const path = join(home, OPERATOR_MANIFEST_FILENAME);
|
|
137
165
|
const tmpPath = join(home, `${OPERATOR_MANIFEST_FILENAME}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
166
|
+
parseOpencodeModelMaps(manifest.defaults);
|
|
138
167
|
writeFileSync(tmpPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
139
168
|
renameSync(tmpPath, path);
|
|
140
169
|
}
|
|
@@ -530,6 +559,9 @@ export function upsertOperatorTarget(manifest, targetPath, appliedVersion, appli
|
|
|
530
559
|
defaults: {
|
|
531
560
|
...manifest.defaults,
|
|
532
561
|
models: { ...manifest.defaults.models },
|
|
562
|
+
...(manifest.defaults.routing !== undefined
|
|
563
|
+
? { routing: manifest.defaults.routing }
|
|
564
|
+
: {}),
|
|
533
565
|
},
|
|
534
566
|
targets,
|
|
535
567
|
},
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Harness } from "./detect.js";
|
|
2
|
+
import type { ModelClass, Profile, Role, Tier } from "./models.js";
|
|
3
|
+
import type { HarnessRouting } from "./routing.js";
|
|
4
|
+
/** Legacy API resolution state: absent is unknown; present missing keys inherit. */
|
|
5
|
+
export interface OpencodeModelMaps {
|
|
6
|
+
opencodeModels?: Partial<Record<Role, string | undefined>>;
|
|
7
|
+
opencodeClassModels?: Partial<Record<ModelClass, string | undefined>>;
|
|
8
|
+
}
|
|
9
|
+
/** Validate compatibility maps independently of strict explicit routing. */
|
|
10
|
+
export declare function parseOpencodeModelMaps(value: {
|
|
11
|
+
opencodeModels?: unknown;
|
|
12
|
+
opencodeClassModels?: unknown;
|
|
13
|
+
}): OpencodeModelMaps;
|
|
14
|
+
/** Persist only the compatibility values that cannot live in strict routing. */
|
|
15
|
+
export declare function legacyOpencodeFallbacks(value: OpencodeModelMaps): OpencodeModelMaps;
|
|
16
|
+
/** Strip compatibility-only leaves before saving or parsing strict routing. */
|
|
17
|
+
export declare function persistableRouting(routing: HarnessRouting): HarnessRouting;
|
|
18
|
+
export interface RoutingScope {
|
|
19
|
+
harnesses: Harness[];
|
|
20
|
+
profile: Profile;
|
|
21
|
+
tiers: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare function selectedTiers(role: Role, tiers: boolean): Tier[];
|
|
24
|
+
/** Selects only leaves that the requested adapters actually install. */
|
|
25
|
+
export declare function selectedRouting(routing: HarnessRouting, scope: RoutingScope): HarnessRouting;
|
|
26
|
+
/** One shared, offline catalog check for setup and installation. */
|
|
27
|
+
export declare function codexCatalogWarnings(routing: HarnessRouting, scope: RoutingScope, catalog?: unknown): string[];
|
|
28
|
+
export declare function legacyRouting(harnesses: Harness[], models: Record<Role, string>, opencodeModels: OpencodeModelMaps["opencodeModels"], opencodeClassModels: OpencodeModelMaps["opencodeClassModels"]): HarnessRouting;
|
|
29
|
+
export interface RoutingStateInput extends OpencodeModelMaps {
|
|
30
|
+
harnesses: Harness[];
|
|
31
|
+
models: Record<Role, string>;
|
|
32
|
+
previousRouting?: HarnessRouting;
|
|
33
|
+
routing?: HarnessRouting;
|
|
34
|
+
routingMode?: "patch" | "replace";
|
|
35
|
+
/** Explicit legacy default-role updates; unrelated selections remain sticky. */
|
|
36
|
+
legacyOverrideRoles?: Role[];
|
|
37
|
+
/** Resolved opencode inputs supplied directly to the installer are updates. */
|
|
38
|
+
updateOpencodeModels?: boolean;
|
|
39
|
+
updateOpencodeClassModels?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/** Materializes legacy inputs and preserves leaves unless explicitly replaced. */
|
|
42
|
+
export declare function normalizeRoutingState(input: RoutingStateInput): HarnessRouting;
|
|
43
|
+
interface ComparableRoutingState extends OpencodeModelMaps {
|
|
44
|
+
models: Partial<Record<Role, string>>;
|
|
45
|
+
routing?: HarnessRouting;
|
|
46
|
+
}
|
|
47
|
+
/** Materialize each precedence layer before a higher layer overrides it. */
|
|
48
|
+
export declare function routingStateLayer(state: ComparableRoutingState & RoutingScope): HarnessRouting;
|
|
49
|
+
/** Whether a missing concrete leaf has an explicit legacy inheritance record. */
|
|
50
|
+
export declare function recordedOpencodeInheritance(maps: OpencodeModelMaps, role: Role, tier: Tier): boolean;
|
|
51
|
+
/** Merge recorded resolution state independently of the active render scope. */
|
|
52
|
+
export declare function mergeRoutingStateLayers(...states: (ComparableRoutingState & RoutingScope)[]): {
|
|
53
|
+
routing: HarnessRouting;
|
|
54
|
+
} & OpencodeModelMaps;
|
|
55
|
+
/** Compares installed scope without consulting a live opencode catalog. */
|
|
56
|
+
export declare function compareRoutingState(repo: ComparableRoutingState, operator: ComparableRoutingState, scope: RoutingScope): {
|
|
57
|
+
differs: boolean;
|
|
58
|
+
gaps: string[];
|
|
59
|
+
};
|
|
60
|
+
export {};
|