orchestrator-workflow 0.27.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.
package/dist/cli.js CHANGED
@@ -5,10 +5,13 @@ import { Command } from "commander";
5
5
  import inquirer from "inquirer";
6
6
  import { PACKAGE_VERSION } from "./assets.js";
7
7
  import { buildApplyInitInputs } from "./cli-apply.js";
8
+ import { buildInitInitInputs } from "./cli-init.js";
8
9
  import { resolveInitInputs } from "./cli-inputs.js";
9
10
  import { HARNESSES, detectHarnesses } from "./detect.js";
10
11
  import { DEFAULT_MODELS, PROFILES } from "./models.js";
11
12
  import { MANIFEST_PATH, readInstalledManifest, runInit } from "./init.js";
13
+ import { parseRouting } from "./routing.js";
14
+ import { codexCatalogWarnings, legacyOpencodeFallbacks, parseOpencodeModelMaps, mergeRoutingStateLayers, } from "./routing-state.js";
12
15
  import { OPERATOR_MANIFEST_FILENAME, OperatorManifestLockTimeoutError, applyRegistrationFailureMessage, createOperatorManifest, operatorManifestState, readOperatorManifest, resolveOperatorHome, safeRealpath, updateOperatorManifest, upsertOperatorTarget, } from "./operator-manifest.js";
13
16
  import { runUninstall } from "./uninstall.js";
14
17
  import { adoptExitCodeForStatus, adoptJsonExtras, inspectTarget, runDoctor, statOrClassify, suppressSuccessLine, targetReportToJson, } from "./doctor.js";
@@ -42,6 +45,20 @@ function requireDirectory(dir) {
42
45
  }
43
46
  return targetDir;
44
47
  }
48
+ function readJsonOption(path, option) {
49
+ try {
50
+ return JSON.parse(readFileSync(resolve(path), "utf8"));
51
+ }
52
+ catch (error) {
53
+ const reason = error instanceof Error ? error.message : String(error);
54
+ throw new Error(`Could not read ${option} JSON file ${path}: ${reason}`);
55
+ }
56
+ }
57
+ function routingOption(path) {
58
+ return path === undefined
59
+ ? undefined
60
+ : parseRouting(readJsonOption(path, "--routing"));
61
+ }
45
62
  /**
46
63
  * `resolveInitInputs` was extracted from `init` and typed against `init`'s
47
64
  * per-repo `Manifest` (kit/version/files/installedAt included), since that
@@ -63,6 +80,8 @@ function defaultsAsManifest(defaults) {
63
80
  models: { ...DEFAULT_MODELS, ...defaults.models },
64
81
  profile: defaults.profile,
65
82
  tiers: defaults.tiers,
83
+ routing: defaults.routing,
84
+ ...parseOpencodeModelMaps(defaults),
66
85
  files: {},
67
86
  installedAt: "",
68
87
  };
@@ -79,6 +98,8 @@ function defaultsEqual(a, b) {
79
98
  profile: d.profile,
80
99
  tiers: d.tiers,
81
100
  models: Object.fromEntries(Object.entries(d.models).sort(([x], [y]) => x.localeCompare(y))),
101
+ routing: d.routing ?? {},
102
+ ...parseOpencodeModelMaps(d),
82
103
  });
83
104
  return normalize(a) === normalize(b);
84
105
  }
@@ -95,6 +116,8 @@ program
95
116
  .option("-f, --force", "overwrite kit-owned files that have local edits")
96
117
  .option("--harness <list>", `comma-separated harnesses (${HARNESSES.join(", ")}), or "none" alone for templates-only mode (.ai/workflow/** and .ai/runs/.gitkeep only, no AGENTS.md/CLAUDE.md/harness files); default: detected`)
97
118
  .option("--models <spec>", 'per-role model overrides, e.g. "implementer=sonnet,reviewer=opus"')
119
+ .option("--routing <json-file>", "harness/role/tier routing patch JSON")
120
+ .option("--codex-catalog <json-file>", "optional offline Codex capability catalog JSON to validate before writing")
98
121
  .option("--profile <profile>", `subagent role profile (${PROFILES.join(", ")}); default: full, or the previously installed profile on a re-run`)
99
122
  .option("--opencode-provider <id>", "opencode provider id for alias resolution (e.g. github-copilot); auto-detected when omitted")
100
123
  .option("--tiers", "also render per-role effort-tier subagent variants (<role>-<tier>.md); default: off, or the previously installed value on a re-run")
@@ -104,6 +127,19 @@ program
104
127
  if (!targetDir)
105
128
  return;
106
129
  const interactive = !opts.yes && isInteractive();
130
+ let routing;
131
+ let codexCatalog;
132
+ try {
133
+ routing = routingOption(opts.routing);
134
+ codexCatalog = opts.codexCatalog
135
+ ? readJsonOption(opts.codexCatalog, "--codex-catalog")
136
+ : undefined;
137
+ }
138
+ catch (error) {
139
+ console.error(error instanceof Error ? error.message : error);
140
+ process.exitCode = 2;
141
+ return;
142
+ }
107
143
  // Say where files will land BEFORE anything is written; an accidental
108
144
  // cwd (e.g. $HOME) is the most likely operator mistake.
109
145
  console.log(`Installing into ${targetDir}`);
@@ -124,17 +160,16 @@ program
124
160
  : "none recorded";
125
161
  console.log(`Found existing install (${version.startsWith("unknown") ? version : `v${version}`}, harnesses: ${installedFor}, profile: ${previous.profile}, tiers: ${previous.tiers})`);
126
162
  }
127
- const { harnesses, profile, models, tiers, opencodeModels, opencodeClassModels, warnings, } = await resolveInitInputs({
128
- detected,
129
- interactive,
130
- previous,
131
- opts,
132
- // `previous` here is `readInstalledManifest(targetDir)` (undefined,
133
- // or the target's own actually-recorded manifest), unlike `apply`'s
134
- // synthetic operator-defaults "floor" object: an empty harnesses
135
- // array is a real recorded --harness none install here.
136
- previousIsRecordedManifest: true,
137
- });
163
+ const { harnesses, profile, models, tiers, opencodeModels, opencodeClassModels, routing: resolvedRouting, warnings, } = await resolveInitInputs(
164
+ // The sticky-branch wiring itself (neither `stickyPreChecked` nor
165
+ // `stickyAnnotateDetected` overridden, so `resolveInitInputs`'s own
166
+ // `?? []` / `?? detected` defaults apply) is pinned inside
167
+ // `buildInitInitInputs` and covered by a dedicated test
168
+ // (`test/cli-init.test.ts`), not by this call site.
169
+ buildInitInitInputs(detected, previous, interactive, {
170
+ ...opts,
171
+ routing,
172
+ }));
138
173
  for (const w of warnings) {
139
174
  process.stderr.write(`${w}\n`);
140
175
  }
@@ -147,6 +182,9 @@ program
147
182
  opencodeModels,
148
183
  tiers,
149
184
  opencodeClassModels,
185
+ routing: resolvedRouting,
186
+ routingMode: "replace",
187
+ codexCatalog,
150
188
  });
151
189
  showPaths("Created", report.written);
152
190
  showPaths("Updated", report.updated);
@@ -162,6 +200,8 @@ program
162
200
  .option("-y, --yes", "accept all defaults and skip prompts")
163
201
  .option("--harness <list>", `comma-separated harnesses (${HARNESSES.join(", ")}); default: previously stored, or claude`)
164
202
  .option("--models <spec>", 'per-role model overrides, e.g. "implementer=sonnet,reviewer=opus"')
203
+ .option("--routing <json-file>", "harness/role/tier routing patch JSON")
204
+ .option("--codex-catalog <json-file>", "optional offline Codex capability catalog JSON to validate before saving")
165
205
  .option("--profile <profile>", `subagent role profile (${PROFILES.join(", ")}); default: full, or the previously stored profile on a re-run`)
166
206
  .option("--opencode-provider <id>", "opencode provider id for alias resolution (e.g. github-copilot); auto-detected when omitted")
167
207
  .option("--tiers", "select per-role effort-tier subagent variants by default; default: off, or the previously stored value on a re-run")
@@ -169,6 +209,19 @@ program
169
209
  .action(async (opts) => {
170
210
  const interactive = !opts.yes && isInteractive();
171
211
  const home = resolveOperatorHome();
212
+ let routingPatch;
213
+ let codexCatalog;
214
+ try {
215
+ routingPatch = routingOption(opts.routing);
216
+ codexCatalog = opts.codexCatalog
217
+ ? readJsonOption(opts.codexCatalog, "--codex-catalog")
218
+ : undefined;
219
+ }
220
+ catch (error) {
221
+ console.error(error instanceof Error ? error.message : error);
222
+ process.exitCode = 2;
223
+ return;
224
+ }
172
225
  console.log(`Operator home: ${home}`);
173
226
  const existing = readOperatorManifest(home);
174
227
  if (existing) {
@@ -180,7 +233,7 @@ program
180
233
  // No target repository exists to detect harnesses against; "claude"
181
234
  // is the same shipped fallback `init` uses for a fresh install with
182
235
  // nothing detected and nothing previously recorded.
183
- const { harnesses, profile, models, tiers, warnings } = await resolveInitInputs({
236
+ const { harnesses, profile, models, tiers, routing, warnings, opencodeModels, opencodeClassModels, } = await resolveInitInputs({
184
237
  // No target directory exists to detect against: the stored
185
238
  // harnesses are the baseline, and only a first-ever setup falls
186
239
  // back to claude, so a codex-only default is not widened silently.
@@ -189,16 +242,30 @@ program
189
242
  : ["claude"],
190
243
  interactive,
191
244
  previous: defaultsAsManifest(existing?.defaults),
192
- opts,
245
+ opts: { ...opts, routing: routingPatch },
193
246
  });
194
247
  for (const w of warnings) {
195
248
  process.stderr.write(`${w}\n`);
196
249
  }
250
+ {
251
+ try {
252
+ for (const warning of codexCatalogWarnings(routing, { harnesses, profile, tiers }, codexCatalog)) {
253
+ process.stderr.write(`Warning: ${warning}\n`);
254
+ }
255
+ }
256
+ catch (error) {
257
+ console.error(error instanceof Error ? error.message : error);
258
+ process.exitCode = 2;
259
+ return;
260
+ }
261
+ }
197
262
  const newDefaults = {
198
263
  harnesses,
199
264
  profile,
200
265
  tiers,
201
266
  models,
267
+ routing,
268
+ ...legacyOpencodeFallbacks({ opencodeModels, opencodeClassModels }),
202
269
  };
203
270
  // The prompts and resolution above ran against `existing`, an
204
271
  // unlocked read taken before this point; another process (an
@@ -413,6 +480,7 @@ function buildApplyPrevious(repoManifest, operatorDefaults, sync) {
413
480
  const tiers = sync
414
481
  ? operatorDefaults.tiers
415
482
  : (repoManifest?.tiers ?? operatorDefaults.tiers);
483
+ const { routing, ...compatibility } = mergeRoutingStateLayers(operatorDefaults, ...(!sync && repoManifest ? [repoManifest] : []));
416
484
  return {
417
485
  kit: "orchestrator-workflow",
418
486
  version: PACKAGE_VERSION,
@@ -427,6 +495,8 @@ function buildApplyPrevious(repoManifest, operatorDefaults, sync) {
427
495
  models,
428
496
  profile,
429
497
  tiers,
498
+ ...(routing !== undefined ? { routing } : {}),
499
+ ...compatibility,
430
500
  files: {},
431
501
  installedAt: "",
432
502
  };
@@ -439,6 +509,8 @@ program
439
509
  .option("-f, --force", "overwrite kit-owned files that have local edits")
440
510
  .option("--harness <list>", `comma-separated harnesses (${HARNESSES.join(", ")}); default: the target's recorded harnesses, else the operator defaults, else detected`)
441
511
  .option("--models <spec>", 'per-role model overrides, e.g. "implementer=sonnet,reviewer=opus"')
512
+ .option("--routing <json-file>", "harness/role/tier routing patch JSON")
513
+ .option("--codex-catalog <json-file>", "optional offline Codex capability catalog JSON to validate before writing")
442
514
  .option("--profile <profile>", `subagent role profile (${PROFILES.join(", ")}); default: the target's recorded profile, else the operator default`)
443
515
  .option("--opencode-provider <id>", "opencode provider id for alias resolution (e.g. github-copilot); auto-detected when omitted")
444
516
  .option("--tiers", "also render per-role effort-tier subagent variants (<role>-<tier>.md); default: the target's recorded value, else the operator default")
@@ -448,6 +520,19 @@ program
448
520
  .option("--pin <version>", "set or replace the target's recorded kit-version pin and apply this operator install regardless of any existing pin")
449
521
  .option("--unpin", "clear the target's recorded kit-version pin and apply this operator install")
450
522
  .action(async (opts) => {
523
+ let routingPatch;
524
+ let codexCatalog;
525
+ try {
526
+ routingPatch = routingOption(opts.routing);
527
+ codexCatalog = opts.codexCatalog
528
+ ? readJsonOption(opts.codexCatalog, "--codex-catalog")
529
+ : undefined;
530
+ }
531
+ catch (error) {
532
+ console.error(error instanceof Error ? error.message : error);
533
+ process.exitCode = 2;
534
+ return;
535
+ }
451
536
  // --pin and --unpin express opposite intents (set a pin vs clear it);
452
537
  // accepting both silently would make the effective pin depend on
453
538
  // internal option-resolution order, so this is a usage error rather
@@ -539,7 +624,7 @@ program
539
624
  }
540
625
  const chosenHarnesses = resolveApplyHarnesses(targetDir, repoManifest, operatorManifest.defaults);
541
626
  const previous = buildApplyPrevious(repoManifest, operatorManifest.defaults, Boolean(opts.sync));
542
- const { harnesses, profile, models, tiers, opencodeModels, opencodeClassModels, warnings, } = await resolveInitInputs(
627
+ const { harnesses, profile, models, tiers, opencodeModels, opencodeClassModels, routing: resolvedRouting, warnings, } = await resolveInitInputs(
543
628
  // `previous` is always defined here (`buildApplyPrevious` returns a
544
629
  // synthetic object even for a target with no manifest of its own),
545
630
  // so `previousIsRecordedManifest` cannot be `Boolean(previous)`; it
@@ -560,7 +645,10 @@ program
560
645
  // `stickyAnnotateDetected`) is pinned inside `buildApplyInitInputs`
561
646
  // rather than inlined here (agent-tasks fe834823, fix round 3,
562
647
  // review finding 1).
563
- buildApplyInitInputs(targetDir, chosenHarnesses, previous, interactive, opts, Boolean(repoManifest)));
648
+ buildApplyInitInputs(targetDir, chosenHarnesses, previous, interactive,
649
+ // Explicit routing is parsed before the first target write and is
650
+ // the final merge layer after repo/operator sticky resolution.
651
+ { ...opts, routing: routingPatch }, Boolean(repoManifest)));
564
652
  for (const w of warnings) {
565
653
  process.stderr.write(`${w}\n`);
566
654
  }
@@ -591,6 +679,9 @@ program
591
679
  opencodeModels,
592
680
  tiers,
593
681
  opencodeClassModels,
682
+ routing: resolvedRouting,
683
+ routingMode: "replace",
684
+ codexCatalog,
594
685
  pin,
595
686
  });
596
687
  showPaths("Created", report.written);
@@ -789,6 +880,12 @@ function printTargetDetail(target, operatorVersion) {
789
880
  if (target.divergence.models) {
790
881
  console.log(` models: ${target.divergentModelRoles.join(", ")}`);
791
882
  }
883
+ if (target.divergence.routing) {
884
+ console.log(" routing: repo and operator selections differ");
885
+ }
886
+ }
887
+ for (const gap of target.routingComparisonGaps ?? []) {
888
+ console.log(` Routing comparison incomplete: ${gap}`);
792
889
  }
793
890
  const showsVersionLagDetail = (target.status === "version-lag" ||
794
891
  ((target.status === "divergent" || target.status === "drift") &&
@@ -826,6 +923,10 @@ function operatorDefaultsFromRepoManifest(repoManifest) {
826
923
  profile: repoManifest.profile,
827
924
  tiers: repoManifest.tiers,
828
925
  models: { ...repoManifest.models },
926
+ ...parseOpencodeModelMaps(repoManifest),
927
+ ...(repoManifest.routing !== undefined
928
+ ? { routing: repoManifest.routing }
929
+ : {}),
829
930
  };
830
931
  }
831
932
  /**
@@ -0,0 +1,8 @@
1
+ import type { Role, Tier } from "./models.js";
2
+ import type { ModelSelection } from "./routing.js";
3
+ /**
4
+ * Produces a standalone Codex subagent definition. The reviewer intentionally
5
+ * inherits its parent's sandbox: its canonical prompt prohibits source edits,
6
+ * while inherited access lets it create test/build output in temporary paths.
7
+ */
8
+ export declare function composeCodexAgent(role: Role, selection: ModelSelection, tier?: Tier): string;
package/dist/codex.js ADDED
@@ -0,0 +1,52 @@
1
+ import { readAgentAsset } from "./assets.js";
2
+ function tomlString(value) {
3
+ let escaped = "";
4
+ for (let index = 0; index < value.length; index += 1) {
5
+ const code = value.charCodeAt(index);
6
+ if (code === 0x22)
7
+ escaped += '\\"';
8
+ else if (code === 0x5c)
9
+ escaped += "\\\\";
10
+ else if (code === 0x08)
11
+ escaped += "\\b";
12
+ else if (code === 0x09)
13
+ escaped += "\\t";
14
+ else if (code === 0x0a)
15
+ escaped += "\\n";
16
+ else if (code === 0x0c)
17
+ escaped += "\\f";
18
+ else if (code === 0x0d)
19
+ escaped += "\\r";
20
+ else if (code < 0x20 || code === 0x7f) {
21
+ escaped += `\\u${code.toString(16).padStart(4, "0")}`;
22
+ }
23
+ else {
24
+ escaped += value[index];
25
+ }
26
+ }
27
+ return `"${escaped}"`;
28
+ }
29
+ function descriptionForTier(description, tier) {
30
+ return tier === undefined
31
+ ? description
32
+ : `${description} (Effort tier: ${tier}.)`;
33
+ }
34
+ /**
35
+ * Produces a standalone Codex subagent definition. The reviewer intentionally
36
+ * inherits its parent's sandbox: its canonical prompt prohibits source edits,
37
+ * while inherited access lets it create test/build output in temporary paths.
38
+ */
39
+ export function composeCodexAgent(role, selection, tier) {
40
+ const asset = readAgentAsset(role);
41
+ const lines = [
42
+ `name = ${tomlString(tier === undefined ? asset.name : `${asset.name}-${tier}`)}`,
43
+ `description = ${tomlString(descriptionForTier(asset.description, tier))}`,
44
+ `model = ${tomlString(selection.model)}`,
45
+ `model_reasoning_effort = ${tomlString(selection.effort)}`,
46
+ ];
47
+ if (role === "explorer" || role === "advisor") {
48
+ lines.push('sandbox_mode = "read-only"');
49
+ }
50
+ lines.push(`developer_instructions = ${tomlString(asset.body.trimEnd())}`);
51
+ return `${lines.join("\n")}\n`;
52
+ }
package/dist/doctor.d.ts CHANGED
@@ -24,6 +24,8 @@ export interface TargetDivergence {
24
24
  profile: boolean;
25
25
  tiers: boolean;
26
26
  models: boolean;
27
+ /** Present only when explicit effective routing differs. */
28
+ routing?: boolean;
27
29
  }
28
30
  /**
29
31
  * Per-target report. Only `path`, `status`, `installedVersion`, `pin`,
@@ -40,6 +42,8 @@ export interface TargetReport {
40
42
  pin: string | null;
41
43
  divergence: TargetDivergence | null;
42
44
  driftFiles: string[] | null;
45
+ /** Selected legacy opencode leaves that cannot be compared offline. */
46
+ routingComparisonGaps?: string[];
43
47
  /** Human-output-only: the repo's own profile, or null when unknown. */
44
48
  repoProfile: Profile | null;
45
49
  /** Human-output-only: the operator default profile, for the comparison line. */
@@ -81,6 +85,8 @@ export interface TargetReportJson {
81
85
  pin: string | null;
82
86
  divergence: TargetDivergence | null;
83
87
  driftFiles: string[] | null;
88
+ /** Selected legacy opencode leaves that cannot be compared offline. */
89
+ routingComparisonGaps?: string[];
84
90
  versionLag: boolean;
85
91
  reason: string | null;
86
92
  }
package/dist/doctor.js CHANGED
@@ -3,7 +3,8 @@ import { existsSync, readFileSync, statSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { PACKAGE_VERSION } from "./assets.js";
5
5
  import { MANIFEST_PATH, readInstalledManifest } from "./init.js";
6
- import { DEFAULT_MODELS, ROLES } from "./models.js";
6
+ import { DEFAULT_MODELS, rolesForProfile } from "./models.js";
7
+ import { compareRoutingState } from "./routing-state.js";
7
8
  import { OPERATOR_MANIFEST_FILENAME, operatorManifestState, updateOperatorManifest, } from "./operator-manifest.js";
8
9
  export function targetReportToJson(report) {
9
10
  return {
@@ -13,6 +14,9 @@ export function targetReportToJson(report) {
13
14
  pin: report.pin,
14
15
  divergence: report.divergence,
15
16
  driftFiles: report.driftFiles,
17
+ ...(report.routingComparisonGaps?.length
18
+ ? { routingComparisonGaps: report.routingComparisonGaps }
19
+ : {}),
16
20
  versionLag: report.versionLag,
17
21
  reason: report.reason,
18
22
  };
@@ -167,17 +171,31 @@ export function inspectTarget(target, operator, kitVersion) {
167
171
  if (manifestStat.kind === "error") {
168
172
  return baseReport(target, operator, "unverifiable", "directory not accessible");
169
173
  }
170
- const manifest = readInstalledManifest(target.path);
174
+ let manifest;
175
+ try {
176
+ manifest = readInstalledManifest(target.path);
177
+ }
178
+ catch {
179
+ // Strict reinstall validation must not abort inspection of other targets.
180
+ return baseReport(target, operator, "unverifiable", "manifest unreadable");
181
+ }
171
182
  if (!manifest) {
172
183
  return baseReport(target, operator, "unverifiable", "manifest unreadable");
173
184
  }
174
185
  const driftFiles = computeDriftFiles(target.path, manifest);
175
- const divergentModelRoles = ROLES.filter((role) => resolvedModel(manifest.models, role) !==
176
- resolvedModel(operator.defaults.models, role));
186
+ const divergentModelRoles = rolesForProfile(manifest.profile).filter((role) => manifest.harnesses.some((harness) => harness === "claude" || harness === "opencode") &&
187
+ resolvedModel(manifest.models, role) !==
188
+ resolvedModel(operator.defaults.models, role));
189
+ const routingComparison = compareRoutingState(manifest, operator.defaults, {
190
+ harnesses: manifest.harnesses,
191
+ profile: manifest.profile,
192
+ tiers: manifest.tiers,
193
+ });
177
194
  const divergence = {
178
195
  profile: manifest.profile !== operator.defaults.profile,
179
196
  tiers: manifest.tiers !== operator.defaults.tiers,
180
197
  models: divergentModelRoles.length > 0,
198
+ ...(routingComparison.differs ? { routing: true } : {}),
181
199
  };
182
200
  // A recorded pin suppresses version-lag only when the pin equals the
183
201
  // repo's own installed version: that is the expected, deliberate-stay
@@ -196,7 +214,10 @@ export function inspectTarget(target, operator, kitVersion) {
196
214
  if (driftFiles.length > 0) {
197
215
  status = "drift";
198
216
  }
199
- else if (divergence.profile || divergence.tiers || divergence.models) {
217
+ else if (divergence.profile ||
218
+ divergence.tiers ||
219
+ divergence.models ||
220
+ divergence.routing) {
200
221
  status = "divergent";
201
222
  }
202
223
  else if (versionLag) {
@@ -212,6 +233,9 @@ export function inspectTarget(target, operator, kitVersion) {
212
233
  pin: hasPin ? manifest.pin : null,
213
234
  divergence,
214
235
  driftFiles: driftFiles.length > 0 ? driftFiles : null,
236
+ ...(routingComparison.gaps.length > 0
237
+ ? { routingComparisonGaps: routingComparison.gaps }
238
+ : {}),
215
239
  repoProfile: manifest.profile,
216
240
  operatorProfile: operator.defaults.profile,
217
241
  repoTiers: manifest.tiers,
package/dist/index.d.ts CHANGED
@@ -8,3 +8,6 @@ export { CLASS_MODELS, DEFAULT_MODELS, DEFAULT_PROFILE, DEFAULT_TIER, MODEL_ALIA
8
8
  export type { ModelAlias, ModelClass, Profile, Role, Tier } from "./models.js";
9
9
  export type { Report } from "./writers.js";
10
10
  export { PACKAGE_VERSION } from "./assets.js";
11
+ export { defaultCodexRouting, mergeRouting, parseRouting, validateCodexCatalog, } from "./routing.js";
12
+ export type { HarnessRouting, ModelSelection } from "./routing.js";
13
+ export { composeCodexAgent } from "./codex.js";
package/dist/index.js CHANGED
@@ -3,3 +3,5 @@ export { runUninstall } from "./uninstall.js";
3
3
  export { detectHarnesses, parseHarnessList, parseHarnessOption, HARNESSES, } from "./detect.js";
4
4
  export { CLASS_MODELS, DEFAULT_MODELS, DEFAULT_PROFILE, DEFAULT_TIER, MODEL_ALIASES, MODEL_CLASSES, PROFILES, ROLES, ROLE_TIERS, TIER_DEFS, claudeModelValue, isProfile, opencodeModelValue, parseModelsSpec, parseProfile, rolesForProfile, } from "./models.js";
5
5
  export { PACKAGE_VERSION } from "./assets.js";
6
+ export { defaultCodexRouting, mergeRouting, parseRouting, validateCodexCatalog, } from "./routing.js";
7
+ export { composeCodexAgent } from "./codex.js";
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
- * When this field is absent the fallback is `opencodeModelValue(models[role])`,
19
- * which passes through a fully-qualified id and returns `undefined` for bare
20
- * aliases, producing the same inherit-session-model behaviour for bare inputs.
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".