orchestrator-workflow 0.28.0 → 0.29.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.
@@ -0,0 +1,277 @@
1
+ import { HARNESSES } from "./detect.js";
2
+ import { CLASS_MODELS, DEFAULT_MODELS, DEFAULT_TIER, MODEL_CLASSES, ROLE_TIERS, ROLES, TIER_DEFS, opencodeModelValue, rolesForProfile, } from "./models.js";
3
+ import { defaultCodexRouting, mergeRouting, parseRouting, validateCodexCatalog, } from "./routing.js";
4
+ /** Validate compatibility maps independently of strict explicit routing. */
5
+ export function parseOpencodeModelMaps(value) {
6
+ const parse = (raw, keys, name) => {
7
+ if (raw === undefined)
8
+ return undefined;
9
+ if (raw === null ||
10
+ typeof raw !== "object" ||
11
+ Array.isArray(raw) ||
12
+ ![Object.prototype, null].includes(Object.getPrototypeOf(raw))) {
13
+ throw new Error(`${name} must be a model map`);
14
+ }
15
+ const result = {};
16
+ for (const [key, model] of Object.entries(raw)) {
17
+ if (!keys.includes(key))
18
+ throw new Error(`Unknown ${name} key "${key}"`);
19
+ if (model === undefined) {
20
+ result[key] = undefined;
21
+ continue;
22
+ }
23
+ if (typeof model !== "string")
24
+ throw new Error(`${name}.${key} must be a model id`);
25
+ // A synthetic provider checks the same safe ID grammar while retaining
26
+ // the bare legacy value, without relaxing parseRouting for user patches.
27
+ parseRouting({
28
+ opencode: {
29
+ implementer: {
30
+ medium: {
31
+ model: model.includes("/") ? model : `legacy/${model}`,
32
+ effort: "medium",
33
+ },
34
+ },
35
+ },
36
+ });
37
+ result[key] = model;
38
+ }
39
+ return result;
40
+ };
41
+ const opencodeModels = parse(value.opencodeModels, ROLES, "opencodeModels");
42
+ const opencodeClassModels = parse(value.opencodeClassModels, MODEL_CLASSES, "opencodeClassModels");
43
+ return {
44
+ ...(opencodeModels !== undefined ? { opencodeModels } : {}),
45
+ ...(opencodeClassModels !== undefined ? { opencodeClassModels } : {}),
46
+ };
47
+ }
48
+ /** Persist only the compatibility values that cannot live in strict routing. */
49
+ export function legacyOpencodeFallbacks(value) {
50
+ const parsed = parseOpencodeModelMaps(value);
51
+ const result = {};
52
+ for (const field of ["opencodeModels", "opencodeClassModels"]) {
53
+ const bare = Object.fromEntries(Object.entries(parsed[field] ?? {}).filter(([, model]) => model !== undefined && !model.includes("/")));
54
+ const keys = field === "opencodeModels" ? ROLES : MODEL_CLASSES;
55
+ const map = parsed[field];
56
+ // Complete qualified maps are represented entirely by routing. Otherwise
57
+ // presence (including {}) records inheritance for missing map keys.
58
+ if (map !== undefined && !keys.every((key) => map[key]?.includes("/")))
59
+ result[field] = bare;
60
+ }
61
+ return result;
62
+ }
63
+ /** Strip compatibility-only leaves before saving or parsing strict routing. */
64
+ export function persistableRouting(routing) {
65
+ const result = mergeRouting(routing);
66
+ for (const role of ROLES) {
67
+ for (const tier of ROLE_TIERS[role]) {
68
+ const model = result.opencode?.[role]?.[tier]?.model;
69
+ if (model !== undefined && !model.includes("/"))
70
+ delete result.opencode?.[role]?.[tier];
71
+ }
72
+ }
73
+ return result;
74
+ }
75
+ export function selectedTiers(role, tiers) {
76
+ return tiers ? ROLE_TIERS[role] : [DEFAULT_TIER[role]];
77
+ }
78
+ /** Selects only leaves that the requested adapters actually install. */
79
+ export function selectedRouting(routing, scope) {
80
+ const result = {};
81
+ for (const harness of scope.harnesses) {
82
+ for (const role of rolesForProfile(scope.profile)) {
83
+ for (const tier of selectedTiers(role, scope.tiers)) {
84
+ const selection = routing[harness]?.[role]?.[tier];
85
+ if (!selection)
86
+ continue;
87
+ result[harness] ??= {};
88
+ result[harness][role] ??= {};
89
+ result[harness][role][tier] = { ...selection };
90
+ }
91
+ }
92
+ }
93
+ return result;
94
+ }
95
+ /** One shared, offline catalog check for setup and installation. */
96
+ export function codexCatalogWarnings(routing, scope, catalog) {
97
+ if (!scope.harnesses.includes("codex"))
98
+ return [];
99
+ if (catalog === undefined) {
100
+ return [
101
+ "Codex model availability and reasoning-effort support were not validated: no capability catalog supplied. The requested selections are used without capability validation.",
102
+ ];
103
+ }
104
+ return validateCodexCatalog(selectedRouting(routing, { ...scope, harnesses: ["codex"] }), catalog);
105
+ }
106
+ export function legacyRouting(harnesses, models, opencodeModels, opencodeClassModels) {
107
+ const routing = {};
108
+ if (harnesses.includes("claude")) {
109
+ routing.claude = {};
110
+ for (const role of rolesForProfile("full")) {
111
+ routing.claude[role] = {
112
+ [DEFAULT_TIER[role]]: {
113
+ model: models[role],
114
+ effort: DEFAULT_TIER[role],
115
+ },
116
+ };
117
+ for (const tier of ROLE_TIERS[role]) {
118
+ if (tier === DEFAULT_TIER[role])
119
+ continue;
120
+ routing.claude[role][tier] = {
121
+ model: CLASS_MODELS[TIER_DEFS[tier].modelClass],
122
+ effort: tier,
123
+ };
124
+ }
125
+ }
126
+ }
127
+ if (harnesses.includes("opencode")) {
128
+ routing.opencode = {};
129
+ for (const role of rolesForProfile("full")) {
130
+ const defaultModel = opencodeModels !== undefined
131
+ ? opencodeModels[role]
132
+ : opencodeModelValue(models[role]);
133
+ if (defaultModel) {
134
+ routing.opencode[role] = {
135
+ [DEFAULT_TIER[role]]: {
136
+ model: defaultModel,
137
+ effort: DEFAULT_TIER[role],
138
+ },
139
+ };
140
+ }
141
+ for (const tier of ROLE_TIERS[role]) {
142
+ if (tier === DEFAULT_TIER[role])
143
+ continue;
144
+ const model = opencodeClassModels?.[TIER_DEFS[tier].modelClass];
145
+ if (!model)
146
+ continue;
147
+ routing.opencode[role] ??= {};
148
+ routing.opencode[role][tier] = { model, effort: tier };
149
+ }
150
+ }
151
+ }
152
+ return routing;
153
+ }
154
+ /** Materializes legacy inputs and preserves leaves unless explicitly replaced. */
155
+ export function normalizeRoutingState(input) {
156
+ const mode = input.routingMode === undefined ? "patch" : input.routingMode;
157
+ if (mode !== "patch" && mode !== "replace") {
158
+ throw new Error('routingMode must be "patch" or "replace"');
159
+ }
160
+ // Validate every caller-supplied layer before merge can drop unknown keys.
161
+ const patch = input.routing === undefined ? undefined : parseRouting(input.routing);
162
+ const previous = input.previousRouting === undefined
163
+ ? undefined
164
+ : parseRouting(input.previousRouting);
165
+ parseOpencodeModelMaps(input);
166
+ const legacy = legacyRouting(input.harnesses, input.models, input.opencodeModels, input.opencodeClassModels);
167
+ const result = mergeRouting(defaultCodexRouting(), legacy, mode === "patch" ? previous : undefined);
168
+ if (mode === "patch") {
169
+ for (const harness of ["claude", "opencode"]) {
170
+ if (!input.harnesses.includes(harness))
171
+ continue;
172
+ for (const role of ROLES) {
173
+ for (const tier of ROLE_TIERS[role]) {
174
+ const defaultTier = tier === DEFAULT_TIER[role];
175
+ const updatesLegacy = defaultTier && input.legacyOverrideRoles?.includes(role);
176
+ const updatesResolved = harness === "opencode" &&
177
+ (defaultTier
178
+ ? input.updateOpencodeModels
179
+ : input.updateOpencodeClassModels);
180
+ if (!updatesLegacy && !updatesResolved)
181
+ continue;
182
+ const selection = legacy[harness]?.[role]?.[tier];
183
+ if (selection) {
184
+ result[harness] ??= {};
185
+ result[harness][role] ??= {};
186
+ result[harness][role][tier] = { ...selection };
187
+ }
188
+ else {
189
+ // Explicitly unresolved legacy inputs restore session inheritance.
190
+ delete result[harness]?.[role]?.[tier];
191
+ }
192
+ }
193
+ }
194
+ }
195
+ }
196
+ return persistableRouting(mergeRouting(result, patch));
197
+ }
198
+ /** Materialize each precedence layer before a higher layer overrides it. */
199
+ export function routingStateLayer(state) {
200
+ const maps = parseOpencodeModelMaps(state);
201
+ const routing = state.routing === undefined ? undefined : parseRouting(state.routing);
202
+ return mergeRouting(selectedRouting(legacyRouting(state.harnesses, { ...DEFAULT_MODELS, ...state.models }, maps.opencodeModels, maps.opencodeClassModels), state),
203
+ // Supplied resolutions also record dormant roles and tiers for later use.
204
+ maps.opencodeModels !== undefined || maps.opencodeClassModels !== undefined
205
+ ? legacyRouting(["opencode"], DEFAULT_MODELS, maps.opencodeModels, maps.opencodeClassModels)
206
+ : undefined, routing);
207
+ }
208
+ /** Whether a missing concrete leaf has an explicit legacy inheritance record. */
209
+ export function recordedOpencodeInheritance(maps, role, tier) {
210
+ return tier === DEFAULT_TIER[role]
211
+ ? maps.opencodeModels !== undefined &&
212
+ maps.opencodeModels[role] === undefined
213
+ : maps.opencodeClassModels !== undefined &&
214
+ maps.opencodeClassModels[TIER_DEFS[tier].modelClass] === undefined;
215
+ }
216
+ /** Merge recorded resolution state independently of the active render scope. */
217
+ export function mergeRoutingStateLayers(...states) {
218
+ let routing = {};
219
+ const maps = {};
220
+ for (const state of states) {
221
+ const nextMaps = parseOpencodeModelMaps(state);
222
+ // A present map records every slot, including missing keys (inheritance).
223
+ // Replace it whole: inactive harnesses, roles, and tiers retain that intent.
224
+ if (nextMaps.opencodeModels !== undefined)
225
+ maps.opencodeModels = { ...nextMaps.opencodeModels };
226
+ if (nextMaps.opencodeClassModels !== undefined)
227
+ maps.opencodeClassModels = { ...nextMaps.opencodeClassModels };
228
+ for (const role of ROLES) {
229
+ for (const tier of ROLE_TIERS[role]) {
230
+ if (recordedOpencodeInheritance(nextMaps, role, tier))
231
+ delete routing.opencode?.[role]?.[tier];
232
+ }
233
+ }
234
+ // The layer's concrete map entries and explicit routing win after masks.
235
+ routing = mergeRouting(routing, routingStateLayer(state));
236
+ }
237
+ return {
238
+ routing: persistableRouting(routing),
239
+ ...legacyOpencodeFallbacks(maps),
240
+ };
241
+ }
242
+ /** Compares installed scope without consulting a live opencode catalog. */
243
+ export function compareRoutingState(repo, operator, scope) {
244
+ const effective = (state) => mergeRouting(defaultCodexRouting(), routingStateLayer({ ...state, ...scope }));
245
+ const actual = effective(repo);
246
+ const expected = effective(operator);
247
+ const actualLegacy = effective({ models: repo.models });
248
+ const expectedLegacy = effective({ models: operator.models });
249
+ const same = (left, right) => left?.model === right?.model && left?.effort === right?.effort;
250
+ let differs = false;
251
+ const gaps = [];
252
+ for (const harness of HARNESSES) {
253
+ if (!scope.harnesses.includes(harness))
254
+ continue;
255
+ for (const role of rolesForProfile(scope.profile)) {
256
+ for (const tier of selectedTiers(role, scope.tiers)) {
257
+ const left = actual[harness]?.[role]?.[tier];
258
+ const right = expected[harness]?.[role]?.[tier];
259
+ if (harness === "opencode" &&
260
+ ((!left && !recordedOpencodeInheritance(repo, role, tier)) ||
261
+ (!right && !recordedOpencodeInheritance(operator, role, tier)))) {
262
+ gaps.push(`opencode/${role}/${tier}: a legacy model lacks a recorded provider id; routing comparison requires an explicit selection.`);
263
+ continue;
264
+ }
265
+ if (!same(left, right)) {
266
+ // A legacy model difference is already reported in divergence.models.
267
+ // Reserve divergence.routing for a difference in the routing layer.
268
+ const explainedByLegacy = same(left, actualLegacy[harness]?.[role]?.[tier]) &&
269
+ same(right, expectedLegacy[harness]?.[role]?.[tier]);
270
+ if (!explainedByLegacy)
271
+ differs = true;
272
+ }
273
+ }
274
+ }
275
+ }
276
+ return { differs, gaps };
277
+ }
@@ -0,0 +1,103 @@
1
+ import type { Harness } from "./detect.js";
2
+ import type { Role, Tier } from "./models.js";
3
+ export interface ModelSelection {
4
+ model: string;
5
+ effort: Tier;
6
+ }
7
+ /**
8
+ * A sparse harness-specific model-routing patch. Later layers supplied to
9
+ * mergeRouting replace a selection leaf while preserving unrelated entries.
10
+ */
11
+ export type HarnessRouting = Partial<Record<Harness, Partial<Record<Role, Partial<Record<Tier, ModelSelection>>>>>>;
12
+ /**
13
+ * Parses one strict, sparse routing layer. It deliberately never drops
14
+ * unrecognised data: a typo in a harness, role, tier, or selection is an
15
+ * error so configuration can be fixed before an installation changes files.
16
+ */
17
+ export declare function parseRouting(value: unknown): HarnessRouting;
18
+ /** Deeply merges sparse routing layers without mutating any input layer. */
19
+ export declare function mergeRouting(...layers: (HarnessRouting | undefined)[]): HarnessRouting;
20
+ declare const CODEX_DEFAULTS: {
21
+ explorer: {
22
+ low: {
23
+ model: string;
24
+ effort: "low";
25
+ };
26
+ medium: {
27
+ model: string;
28
+ effort: "medium";
29
+ };
30
+ high: {
31
+ model: string;
32
+ effort: "high";
33
+ };
34
+ };
35
+ "task-slicer": {
36
+ low: {
37
+ model: string;
38
+ effort: "low";
39
+ };
40
+ medium: {
41
+ model: string;
42
+ effort: "medium";
43
+ };
44
+ high: {
45
+ model: string;
46
+ effort: "high";
47
+ };
48
+ };
49
+ implementer: {
50
+ low: {
51
+ model: string;
52
+ effort: "low";
53
+ };
54
+ medium: {
55
+ model: string;
56
+ effort: "medium";
57
+ };
58
+ high: {
59
+ model: string;
60
+ effort: "high";
61
+ };
62
+ xhigh: {
63
+ model: string;
64
+ effort: "xhigh";
65
+ };
66
+ };
67
+ reviewer: {
68
+ medium: {
69
+ model: string;
70
+ effort: "medium";
71
+ };
72
+ high: {
73
+ model: string;
74
+ effort: "high";
75
+ };
76
+ xhigh: {
77
+ model: string;
78
+ effort: "xhigh";
79
+ };
80
+ };
81
+ advisor: {
82
+ high: {
83
+ model: string;
84
+ effort: "high";
85
+ };
86
+ xhigh: {
87
+ model: string;
88
+ effort: "xhigh";
89
+ };
90
+ };
91
+ };
92
+ export type CodexDefaultRouting = HarnessRouting & {
93
+ codex: typeof CODEX_DEFAULTS;
94
+ };
95
+ /** Returns a fresh complete Codex routing layer, including each default tier. */
96
+ export declare function defaultCodexRouting(): CodexDefaultRouting;
97
+ /**
98
+ * Validates the selected Codex leaves against a caller-provided debug-model
99
+ * catalog. This helper makes no CLI or network call; callers decide how to
100
+ * acquire the catalog and which installed roles and tiers to pass in.
101
+ */
102
+ export declare function validateCodexCatalog(routing: HarnessRouting, catalog: unknown): string[];
103
+ export {};
@@ -0,0 +1,254 @@
1
+ import { HARNESSES } from "./detect.js";
2
+ import { ROLE_TIERS, ROLES } from "./models.js";
3
+ const TIERS = ["low", "medium", "high", "xhigh"];
4
+ const DANGEROUS_KEYS = new Set(["__proto__", "prototype", "constructor"]);
5
+ const CLAUDE_ALIASES = new Set(["sonnet", "opus", "haiku"]);
6
+ const YAML_IMPLICIT_SCALARS = new Set([
7
+ "true",
8
+ "false",
9
+ "null",
10
+ "yes",
11
+ "no",
12
+ "on",
13
+ "off",
14
+ ]);
15
+ function isRecord(value) {
16
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
17
+ return false;
18
+ }
19
+ const prototype = Object.getPrototypeOf(value);
20
+ return prototype === Object.prototype || prototype === null;
21
+ }
22
+ function assertSafeKeys(value, location) {
23
+ for (const key of Object.keys(value)) {
24
+ if (DANGEROUS_KEYS.has(key)) {
25
+ throw new Error(`Dangerous key "${key}" at ${location}`);
26
+ }
27
+ }
28
+ }
29
+ function assertKnownKey(key, known, location) {
30
+ if (!known.includes(key)) {
31
+ throw new Error(`Unknown ${location} "${key}"; valid values: ${known.join(", ")}`);
32
+ }
33
+ }
34
+ function assertModelId(model, harness, location) {
35
+ if (typeof model !== "string" ||
36
+ model.length === 0 ||
37
+ model !== model.trim()) {
38
+ throw new Error(`${location}.model must be a non-empty trimmed string`);
39
+ }
40
+ if (/\s|[\u0000-\u001f\u007f]/.test(model)) {
41
+ throw new Error(`${location}.model must not contain whitespace or control characters`);
42
+ }
43
+ const idPattern = harness === "opencode"
44
+ ? /^[A-Za-z][A-Za-z0-9._/@+-]*(?::[A-Za-z0-9][A-Za-z0-9._/@+-]*)*$/
45
+ : /^[A-Za-z][A-Za-z0-9._/@+-]*$/;
46
+ if (!idPattern.test(model)) {
47
+ throw new Error(`${location}.model must be a plain model id using letters, numbers, dots, underscores, slashes, at signs, pluses, hyphens, and (for opencode) version colons`);
48
+ }
49
+ if (YAML_IMPLICIT_SCALARS.has(model.toLowerCase())) {
50
+ throw new Error(`${location}.model must not be a YAML implicit scalar`);
51
+ }
52
+ if (harness === "codex" && CLAUDE_ALIASES.has(model)) {
53
+ throw new Error(`${location}.model must be a Codex model id, not Claude alias "${model}"`);
54
+ }
55
+ if (harness === "opencode" &&
56
+ (!model.includes("/") || model.startsWith("/") || model.endsWith("/"))) {
57
+ throw new Error(`${location}.model must be a qualified provider/model id for explicit opencode routing`);
58
+ }
59
+ return model;
60
+ }
61
+ function parseSelection(value, harness, location) {
62
+ if (!isRecord(value)) {
63
+ throw new Error(`${location} must be a selection object`);
64
+ }
65
+ assertSafeKeys(value, location);
66
+ const keys = Object.keys(value);
67
+ for (const key of keys) {
68
+ if (key !== "model" && key !== "effort") {
69
+ throw new Error(`Unknown ${location} field "${key}"; expected model, effort`);
70
+ }
71
+ }
72
+ if (!("model" in value) || !("effort" in value)) {
73
+ throw new Error(`${location} must contain both model and effort`);
74
+ }
75
+ const model = assertModelId(value.model, harness, location);
76
+ if (typeof value.effort !== "string") {
77
+ throw new Error(`${location}.effort must be one of ${TIERS.join(", ")}`);
78
+ }
79
+ assertKnownKey(value.effort, TIERS, `${location}.effort`);
80
+ return { model, effort: value.effort };
81
+ }
82
+ /**
83
+ * Parses one strict, sparse routing layer. It deliberately never drops
84
+ * unrecognised data: a typo in a harness, role, tier, or selection is an
85
+ * error so configuration can be fixed before an installation changes files.
86
+ */
87
+ export function parseRouting(value) {
88
+ if (!isRecord(value)) {
89
+ throw new Error("Routing must be an object");
90
+ }
91
+ assertSafeKeys(value, "routing");
92
+ const result = {};
93
+ for (const [harnessName, harnessValue] of Object.entries(value)) {
94
+ assertKnownKey(harnessName, HARNESSES, "harness");
95
+ if (!isRecord(harnessValue)) {
96
+ throw new Error(`Routing for harness "${harnessName}" must be an object`);
97
+ }
98
+ assertSafeKeys(harnessValue, `routing.${harnessName}`);
99
+ const roleRouting = {};
100
+ for (const [roleName, roleValue] of Object.entries(harnessValue)) {
101
+ assertKnownKey(roleName, ROLES, `role for harness "${harnessName}"`);
102
+ if (!isRecord(roleValue)) {
103
+ throw new Error(`Routing for ${harnessName}.${roleName} must be an object`);
104
+ }
105
+ assertSafeKeys(roleValue, `routing.${harnessName}.${roleName}`);
106
+ const tierRouting = {};
107
+ for (const [tierName, selection] of Object.entries(roleValue)) {
108
+ assertKnownKey(tierName, TIERS, `tier for ${harnessName}.${roleName}`);
109
+ if (!ROLE_TIERS[roleName].includes(tierName)) {
110
+ throw new Error(`Tier "${tierName}" is not allowed for role "${roleName}"; allowed tiers: ${ROLE_TIERS[roleName].join(", ")}`);
111
+ }
112
+ tierRouting[tierName] = parseSelection(selection, harnessName, `routing.${harnessName}.${roleName}.${tierName}`);
113
+ }
114
+ roleRouting[roleName] = tierRouting;
115
+ }
116
+ result[harnessName] = roleRouting;
117
+ }
118
+ return result;
119
+ }
120
+ /** Deeply merges sparse routing layers without mutating any input layer. */
121
+ export function mergeRouting(...layers) {
122
+ const result = {};
123
+ for (const layer of layers) {
124
+ if (layer === undefined)
125
+ continue;
126
+ for (const harness of HARNESSES) {
127
+ const harnessLayer = layer[harness];
128
+ if (harnessLayer === undefined)
129
+ continue;
130
+ const resultHarness = (result[harness] ??= {});
131
+ for (const role of ROLES) {
132
+ const roleLayer = harnessLayer[role];
133
+ if (roleLayer === undefined)
134
+ continue;
135
+ const resultRole = (resultHarness[role] ??= {});
136
+ for (const tier of ROLE_TIERS[role]) {
137
+ const selection = roleLayer[tier];
138
+ if (selection !== undefined) {
139
+ resultRole[tier] = { ...selection };
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ return result;
146
+ }
147
+ const CODEX_DEFAULTS = {
148
+ explorer: {
149
+ low: { model: "gpt-5.6-luna", effort: "low" },
150
+ medium: { model: "gpt-5.6-sol", effort: "medium" },
151
+ high: { model: "gpt-5.6-sol", effort: "high" },
152
+ },
153
+ "task-slicer": {
154
+ low: { model: "gpt-5.6-luna", effort: "low" },
155
+ medium: { model: "gpt-5.6-sol", effort: "medium" },
156
+ high: { model: "gpt-5.6-sol", effort: "high" },
157
+ },
158
+ implementer: {
159
+ low: { model: "gpt-5.6-luna", effort: "low" },
160
+ medium: { model: "gpt-5.6-terra", effort: "medium" },
161
+ high: { model: "gpt-5.6-terra", effort: "high" },
162
+ xhigh: { model: "gpt-6-astra", effort: "xhigh" },
163
+ },
164
+ reviewer: {
165
+ medium: { model: "gpt-5.6-terra", effort: "medium" },
166
+ high: { model: "gpt-6-astra", effort: "high" },
167
+ xhigh: { model: "gpt-6-astra", effort: "xhigh" },
168
+ },
169
+ advisor: {
170
+ high: { model: "gpt-6-astra", effort: "high" },
171
+ xhigh: { model: "gpt-6-astra", effort: "xhigh" },
172
+ },
173
+ };
174
+ /** Returns a fresh complete Codex routing layer, including each default tier. */
175
+ export function defaultCodexRouting() {
176
+ return mergeRouting({ codex: CODEX_DEFAULTS });
177
+ }
178
+ function normalizeCodexCatalog(catalog) {
179
+ const modelsValue = Array.isArray(catalog)
180
+ ? catalog
181
+ : isRecord(catalog)
182
+ ? catalog.models
183
+ : undefined;
184
+ if (!Array.isArray(modelsValue)) {
185
+ throw new Error("Codex catalog must be an array of models or an object with a models array");
186
+ }
187
+ const models = new Map();
188
+ for (const [index, modelValue] of modelsValue.entries()) {
189
+ if (!isRecord(modelValue) ||
190
+ typeof modelValue.slug !== "string" ||
191
+ modelValue.slug.trim() === "") {
192
+ throw new Error(`Codex catalog model at index ${index} must have a non-empty slug`);
193
+ }
194
+ if (models.has(modelValue.slug)) {
195
+ throw new Error(`Codex catalog contains duplicate model slug "${modelValue.slug}"`);
196
+ }
197
+ const support = modelValue.supported_reasoning_levels;
198
+ if (support === undefined) {
199
+ models.set(modelValue.slug, {
200
+ slug: modelValue.slug,
201
+ supportWarning: `Codex catalog does not provide reasoning-effort support for "${modelValue.slug}"; its selected effort could not be validated.`,
202
+ });
203
+ continue;
204
+ }
205
+ if (!Array.isArray(support)) {
206
+ throw new Error(`Codex catalog model "${modelValue.slug}" has malformed supported_reasoning_levels`);
207
+ }
208
+ const efforts = new Set();
209
+ for (const level of support) {
210
+ if (!isRecord(level) || typeof level.effort !== "string") {
211
+ throw new Error(`Codex catalog model "${modelValue.slug}" has malformed reasoning-effort support`);
212
+ }
213
+ efforts.add(level.effort);
214
+ }
215
+ models.set(modelValue.slug, {
216
+ slug: modelValue.slug,
217
+ supportedEfforts: efforts,
218
+ });
219
+ }
220
+ return models;
221
+ }
222
+ /**
223
+ * Validates the selected Codex leaves against a caller-provided debug-model
224
+ * catalog. This helper makes no CLI or network call; callers decide how to
225
+ * acquire the catalog and which installed roles and tiers to pass in.
226
+ */
227
+ export function validateCodexCatalog(routing, catalog) {
228
+ const codexRouting = routing.codex;
229
+ if (codexRouting === undefined)
230
+ return [];
231
+ const models = normalizeCodexCatalog(catalog);
232
+ const warnings = new Set();
233
+ for (const role of ROLES) {
234
+ const roleRouting = codexRouting[role];
235
+ if (roleRouting === undefined)
236
+ continue;
237
+ for (const tier of ROLE_TIERS[role]) {
238
+ const selection = roleRouting[tier];
239
+ if (selection === undefined)
240
+ continue;
241
+ const model = models.get(selection.model);
242
+ if (model === undefined) {
243
+ throw new Error(`Codex model "${selection.model}" selected for ${role}/${tier} is unavailable in the supplied catalog`);
244
+ }
245
+ if (model.supportWarning !== undefined) {
246
+ warnings.add(model.supportWarning);
247
+ }
248
+ else if (!model.supportedEfforts?.has(selection.effort)) {
249
+ throw new Error(`Codex model "${selection.model}" does not support reasoning effort "${selection.effort}" selected for ${role}/${tier}`);
250
+ }
251
+ }
252
+ }
253
+ return [...warnings];
254
+ }
package/dist/uninstall.js CHANGED
@@ -81,6 +81,8 @@ const PRUNE_CANDIDATES = [
81
81
  join(".agents", "skills", "orchestrator-workflow"),
82
82
  join(".agents", "skills"),
83
83
  ".agents",
84
+ join(".codex", "agents"),
85
+ ".codex",
84
86
  join(".opencode", "skills", "orchestrator-workflow"),
85
87
  join(".opencode", "skills"),
86
88
  join(".opencode", "agents"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orchestrator-workflow",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Installer for an orchestrator-led agent workflow: .ai/ run state, an AGENTS.md policy section, and per-harness subagent definitions for Claude Code, OpenAI Codex, and opencode",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -50,6 +50,7 @@
50
50
  "inquirer": "^9.2.0"
51
51
  },
52
52
  "devDependencies": {
53
+ "@iarna/toml": "^2.2.5",
53
54
  "@types/inquirer": "^9.0.7",
54
55
  "@types/node": "^20.11.0",
55
56
  "prettier": "^3.8.1",