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.
- package/CHANGELOG.md +26 -0
- package/INSTALL-AGENT.md +85 -58
- package/README.md +134 -44
- package/assets/agents-md-section.md +16 -4
- package/assets/skill/SKILL.md +17 -6
- 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/cli-apply.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ResolveInitInputsParams } from "./cli-inputs.js";
|
|
2
2
|
import type { Harness } from "./detect.js";
|
|
3
3
|
import type { Manifest } from "./init.js";
|
|
4
|
+
import type { HarnessRouting } from "./routing.js";
|
|
4
5
|
/** The subset of `apply`'s commander options that feed input resolution. */
|
|
5
6
|
export interface ApplyResolutionOptions {
|
|
6
7
|
harness?: string;
|
|
@@ -8,6 +9,7 @@ export interface ApplyResolutionOptions {
|
|
|
8
9
|
profile?: string;
|
|
9
10
|
opencodeProvider?: string;
|
|
10
11
|
tiers?: boolean;
|
|
12
|
+
routing?: HarnessRouting;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* Builds `apply`'s own `resolveInitInputs` params, pinning the sticky-branch
|
package/dist/cli-inputs.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Harness } from "./detect.js";
|
|
2
2
|
import type { Manifest } from "./init.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { Profile, Role } from "./models.js";
|
|
4
|
+
import type { OpencodeModelMaps } from "./routing-state.js";
|
|
5
|
+
import type { HarnessRouting } from "./routing.js";
|
|
4
6
|
export declare function promptHarnesses(detected: Harness[], installed: Harness[], fallbackToClaude?: boolean, annotateDetected?: Harness[]): Promise<Harness[]>;
|
|
5
7
|
export declare function promptProfile(base: Profile): Promise<Profile>;
|
|
6
8
|
export declare function promptModels(base: Record<Role, string>, roles: Role[]): Promise<Record<Role, string>>;
|
|
@@ -11,6 +13,7 @@ export interface InitResolutionOptions {
|
|
|
11
13
|
profile?: string;
|
|
12
14
|
opencodeProvider?: string;
|
|
13
15
|
tiers?: boolean;
|
|
16
|
+
routing?: HarnessRouting;
|
|
14
17
|
}
|
|
15
18
|
export interface ResolveInitInputsParams {
|
|
16
19
|
/** Result of `detectHarnesses(targetDir)`; passed in so the caller can
|
|
@@ -52,15 +55,14 @@ export interface ResolveInitInputsParams {
|
|
|
52
55
|
* The entries pre-checked in the interactive prompt when the target
|
|
53
56
|
* recorded `harnesses: []` (the harnesses-stickiness gate's branch,
|
|
54
57
|
* gated on `previousIsRecordedManifest && previous.
|
|
55
|
-
* harnessesRecordedEmpty`). Defaults to `[]` when omitted
|
|
56
|
-
* `apply` share this semantics
|
|
58
|
+
* harnessesRecordedEmpty`). Defaults to `[]` when omitted, so `init` and
|
|
59
|
+
* `apply` share this semantics. A fresh
|
|
57
60
|
* interactive re-run on a templates-only target starts with nothing
|
|
58
61
|
* pre-checked, because the recorded `harnesses: []` is the intent that
|
|
59
62
|
* matters, not a `.claude/`-style directory the harness itself left on
|
|
60
63
|
* disk, which is a weak signal and must not re-widen a deliberate
|
|
61
|
-
* `--harness none` install just because a bare Enter is pressed
|
|
62
|
-
*
|
|
63
|
-
* `init`). `apply`'s call site still passes `[]` explicitly, as defence
|
|
64
|
+
* `--harness none` install just because a bare Enter is pressed. `apply`'s
|
|
65
|
+
* call site still passes `[]` explicitly, as defence
|
|
64
66
|
* in depth (see `buildApplyInitInputs`'s doc comment). Only the sticky
|
|
65
67
|
* branch reads this field; the normal (non-recorded-empty) branch still
|
|
66
68
|
* prompts from `detected` unchanged, matching `apply`'s existing
|
|
@@ -75,7 +77,7 @@ export interface ResolveInitInputsParams {
|
|
|
75
77
|
* `stickyPreChecked`'s doc comment), the operator still sees which
|
|
76
78
|
* harness is actually on disk, because labelling is a hint, not an
|
|
77
79
|
* intent signal, so it is safe to annotate what the pre-check itself
|
|
78
|
-
* must not read
|
|
80
|
+
* must not read. `init`'s call site
|
|
79
81
|
* omits this field and gets its own `detectHarnesses(targetDir)` result
|
|
80
82
|
* via this default; `apply`'s call site passes a fresh
|
|
81
83
|
* `detectHarnesses(targetDir)` call explicitly, since its own
|
|
@@ -85,13 +87,12 @@ export interface ResolveInitInputsParams {
|
|
|
85
87
|
*/
|
|
86
88
|
stickyAnnotateDetected?: Harness[];
|
|
87
89
|
}
|
|
88
|
-
export interface ResolvedInitInputs {
|
|
90
|
+
export interface ResolvedInitInputs extends OpencodeModelMaps {
|
|
89
91
|
harnesses: Harness[];
|
|
90
92
|
profile: Profile;
|
|
91
93
|
models: Record<Role, string>;
|
|
92
94
|
tiers: boolean;
|
|
93
|
-
|
|
94
|
-
opencodeClassModels?: Record<ModelClass, string | undefined>;
|
|
95
|
+
routing: HarnessRouting;
|
|
95
96
|
/**
|
|
96
97
|
* Warning lines to print, in order, exactly as `init` printed them to
|
|
97
98
|
* stderr before this extraction (each written as `${line}\n`). Returned
|
|
@@ -112,12 +113,7 @@ export interface ResolvedInitInputs {
|
|
|
112
113
|
* omitted) keeps the previously installed value; a fresh install with no
|
|
113
114
|
* prior manifest falls back to the shipped default.
|
|
114
115
|
*
|
|
115
|
-
* `params.detected` is the fallback-chain input
|
|
116
|
-
*
|
|
117
|
-
* harnesses-stickiness branch's own pre-check any more: that branch
|
|
118
|
-
* always pre-checks `stickyPreChecked ?? []` (D-002, agent-dx 7669907c),
|
|
119
|
-
* never `detected`, so `init` and `apply` share one semantics there. It
|
|
120
|
-
* still feeds that branch's " (detected)" label by default (see
|
|
121
|
-
* `ResolveInitInputsParams.stickyAnnotateDetected`'s doc comment).
|
|
116
|
+
* `params.detected` is the non-sticky fallback-chain input; see
|
|
117
|
+
* `ResolveInitInputsParams.stickyPreChecked` for the sticky rule.
|
|
122
118
|
*/
|
|
123
119
|
export declare function resolveInitInputs(params: ResolveInitInputsParams): Promise<ResolvedInitInputs>;
|
package/dist/cli-inputs.js
CHANGED
|
@@ -1,35 +1,16 @@
|
|
|
1
1
|
import inquirer from "inquirer";
|
|
2
2
|
import { HARNESSES, parseHarnessOption } from "./detect.js";
|
|
3
|
-
import { CLASS_MODELS, DEFAULT_MODELS, DEFAULT_PROFILE, MODEL_ALIASES, MODEL_CLASSES, assertValidModelId, parseModelsSpec, parseProfile, rolesForProfile, } from "./models.js";
|
|
3
|
+
import { CLASS_MODELS, DEFAULT_TIER, DEFAULT_MODELS, DEFAULT_PROFILE, MODEL_ALIASES, MODEL_CLASSES, assertValidModelId, parseModelsSpec, parseProfile, rolesForProfile, } from "./models.js";
|
|
4
|
+
import { mergeRouting, parseRouting } from "./routing.js";
|
|
5
|
+
import { legacyRouting, normalizeRoutingState, selectedTiers, parseOpencodeModelMaps, recordedOpencodeInheritance, } from "./routing-state.js";
|
|
4
6
|
import { detectProvider, loadOpencodeCatalog, resolveAlias, resolveOpencodeModels, } from "./opencode.js";
|
|
5
7
|
export async function promptHarnesses(detected, installed, fallbackToClaude = true,
|
|
6
8
|
// Drives only the checkbox's " (detected)" label suffix, independent of
|
|
7
|
-
// `detected`'s
|
|
8
|
-
// so a call site that omits this parameter keeps annotating exactly what
|
|
9
|
-
// it pre-checks from, unchanged. `resolveInitInputs`'s harnesses-
|
|
10
|
-
// stickiness branch (shared by `init` and `apply`, D-002, agent-dx
|
|
11
|
-
// 7669907c) always passes an explicit value here (`stickyAnnotateDetected
|
|
12
|
-
// ?? detected`) instead of relying on this default: that branch pre-checks
|
|
13
|
-
// nothing but still wants the operator to see which harness is actually
|
|
14
|
-
// on disk, so labelling and pre-checking are resolved separately
|
|
15
|
-
// (agent-tasks fe834823, fix round 3).
|
|
9
|
+
// `detected`'s role in pre-checking; see `stickyAnnotateDetected` below.
|
|
16
10
|
annotateDetected = detected) {
|
|
17
11
|
const known = [...new Set([...detected, ...installed])];
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// `true`). The templates-only re-run branch below opts OUT of that
|
|
21
|
-
// (`fallbackToClaude: false`): a repo the operator explicitly recorded as
|
|
22
|
-
// `harnesses: []` has no harness files by construction, and pre-checking
|
|
23
|
-
// `claude` on Enter would silently re-widen an explicit `--harness none`
|
|
24
|
-
// install -- contradicting README.md's and this function's own "nothing
|
|
25
|
-
// forced pre-selected" claim (see CHANGELOG). `resolveInitInputs`'s
|
|
26
|
-
// harnesses-stickiness branch (shared by `init` and `apply`, D-002,
|
|
27
|
-
// agent-dx 7669907c) always calls this function with `stickyPreChecked ??
|
|
28
|
-
// []` regardless of what is actually on disk or previously chosen --
|
|
29
|
-
// the operator's recorded `harnesses: []` is the intent that matters,
|
|
30
|
-
// not a harness config a harness itself left behind (agent-tasks
|
|
31
|
-
// fe834823; the residual noted in docs/okf/log.md's 2026-08-31 entry is
|
|
32
|
-
// closed).
|
|
12
|
+
// The templates-only branch disables this first-run fallback; see
|
|
13
|
+
// `ResolveInitInputsParams.stickyPreChecked` for the sticky rule.
|
|
33
14
|
const preselected = known.length > 0 ? known : fallbackToClaude ? ["claude"] : [];
|
|
34
15
|
const { harnesses } = await inquirer.prompt([
|
|
35
16
|
{
|
|
@@ -118,6 +99,16 @@ export async function promptModels(base, roles) {
|
|
|
118
99
|
}
|
|
119
100
|
return models;
|
|
120
101
|
}
|
|
102
|
+
function modelOverrideRoles(spec) {
|
|
103
|
+
const roles = [];
|
|
104
|
+
for (const item of spec.split(",")) {
|
|
105
|
+
const role = item.trim().split("=", 1)[0]?.trim();
|
|
106
|
+
if (role && rolesForProfile("full").includes(role)) {
|
|
107
|
+
roles.push(role);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return [...new Set(roles)];
|
|
111
|
+
}
|
|
121
112
|
/**
|
|
122
113
|
* Resolves everything `runInit` needs (harnesses, profile, models, tiers,
|
|
123
114
|
* the opencode model resolutions) from the CLI-parsed options, the target
|
|
@@ -130,16 +121,15 @@ export async function promptModels(base, roles) {
|
|
|
130
121
|
* omitted) keeps the previously installed value; a fresh install with no
|
|
131
122
|
* prior manifest falls back to the shipped default.
|
|
132
123
|
*
|
|
133
|
-
* `params.detected` is the fallback-chain input
|
|
134
|
-
*
|
|
135
|
-
* harnesses-stickiness branch's own pre-check any more: that branch
|
|
136
|
-
* always pre-checks `stickyPreChecked ?? []` (D-002, agent-dx 7669907c),
|
|
137
|
-
* never `detected`, so `init` and `apply` share one semantics there. It
|
|
138
|
-
* still feeds that branch's " (detected)" label by default (see
|
|
139
|
-
* `ResolveInitInputsParams.stickyAnnotateDetected`'s doc comment).
|
|
124
|
+
* `params.detected` is the non-sticky fallback-chain input; see
|
|
125
|
+
* `ResolveInitInputsParams.stickyPreChecked` for the sticky rule.
|
|
140
126
|
*/
|
|
141
127
|
export async function resolveInitInputs(params) {
|
|
142
128
|
const { detected, interactive, previous, opts, previousIsRecordedManifest, stickyPreChecked, stickyAnnotateDetected, } = params;
|
|
129
|
+
const routingPatch = opts.routing === undefined ? undefined : parseRouting(opts.routing);
|
|
130
|
+
const previousRouting = previous?.routing === undefined
|
|
131
|
+
? undefined
|
|
132
|
+
: parseRouting(previous.routing);
|
|
143
133
|
let harnesses;
|
|
144
134
|
if (opts.harness) {
|
|
145
135
|
harnesses = parseHarnessOption(opts.harness);
|
|
@@ -171,19 +161,7 @@ export async function resolveInitInputs(params) {
|
|
|
171
161
|
// as `[]` (not the recorded `previous.harnesses`) so nothing is
|
|
172
162
|
// pre-checked from the previous install, unlike the "else" branch
|
|
173
163
|
// below's normal re-run prompt.
|
|
174
|
-
// `stickyPreChecked
|
|
175
|
-
// `apply` share this semantics (D-002, agent-dx 7669907c; see
|
|
176
|
-
// CHANGELOG). Neither call site needs to pass `stickyPreChecked`
|
|
177
|
-
// itself for this default to apply (`apply` still does, as defence in
|
|
178
|
-
// depth; see `buildApplyInitInputs`'s doc comment); a bare Enter on a
|
|
179
|
-
// target recorded as `harnesses: []` must not re-widen a deliberate
|
|
180
|
-
// `--harness none` install just because a harness config happens to
|
|
181
|
-
// still be on disk, which is a weak signal, not the recorded intent
|
|
182
|
-
// (agent-tasks fe834823; the same argument applies identically to
|
|
183
|
-
// `init`). `fallbackToClaude: false` closes the same gap for the case
|
|
184
|
-
// where nothing is pre-checked either: without it, `promptHarnesses`
|
|
185
|
-
// would pre-check `claude` on its own "nothing known" fallback,
|
|
186
|
-
// re-widening the install on a bare Enter.
|
|
164
|
+
// See `ResolveInitInputsParams.stickyPreChecked` for the shared sticky rule.
|
|
187
165
|
// `stickyAnnotateDetected ?? detected` is passed through as the fourth
|
|
188
166
|
// argument so the checkbox's " (detected)" label still points at what
|
|
189
167
|
// is actually on disk even though nothing is pre-checked from it: the
|
|
@@ -226,8 +204,16 @@ export async function resolveInitInputs(params) {
|
|
|
226
204
|
};
|
|
227
205
|
if (opts.models)
|
|
228
206
|
models = parseModelsSpec(opts.models, models);
|
|
229
|
-
|
|
207
|
+
const promptLegacyModels = interactive &&
|
|
208
|
+
!opts.models &&
|
|
209
|
+
harnesses.some((harness) => harness === "claude" || harness === "opencode");
|
|
210
|
+
if (promptLegacyModels)
|
|
230
211
|
models = await promptModels(models, rolesForProfile(profile));
|
|
212
|
+
const legacyOverrideRoles = opts.models
|
|
213
|
+
? modelOverrideRoles(opts.models)
|
|
214
|
+
: promptLegacyModels
|
|
215
|
+
? rolesForProfile(profile)
|
|
216
|
+
: [];
|
|
231
217
|
// Explicit --tiers/--no-tiers always override; a plain re-run (neither
|
|
232
218
|
// flag passed) keeps whatever the previous install had (default false
|
|
233
219
|
// for a fresh install), same override-vs-persist rule as
|
|
@@ -242,16 +228,28 @@ export async function resolveInitInputs(params) {
|
|
|
242
228
|
// Resolve opencode model aliases against the live catalog when the opencode
|
|
243
229
|
// harness is selected. The shell-out stays reachable only from this
|
|
244
230
|
// resolution step, keeping runInit pure.
|
|
245
|
-
|
|
246
|
-
let
|
|
231
|
+
const previousMaps = parseOpencodeModelMaps(previous ?? {});
|
|
232
|
+
let opencodeModels = previousMaps.opencodeModels;
|
|
233
|
+
let opencodeClassModels = previousMaps.opencodeClassModels;
|
|
247
234
|
const warnings = [];
|
|
248
|
-
|
|
235
|
+
const availableRouting = mergeRouting(legacyRouting(harnesses, models, opencodeModels, opencodeClassModels), previousRouting, routingPatch);
|
|
236
|
+
const needsOpencodeResolution = harnesses.includes("opencode") &&
|
|
237
|
+
(opts.opencodeProvider !== undefined ||
|
|
238
|
+
legacyOverrideRoles.some((role) => !routingPatch?.opencode?.[role]?.[DEFAULT_TIER[role]]) ||
|
|
239
|
+
rolesForProfile(profile).some((role) => selectedTiers(role, tiers).some((tier) => !availableRouting.opencode?.[role]?.[tier] &&
|
|
240
|
+
!recordedOpencodeInheritance(previousMaps, role, tier))));
|
|
241
|
+
if (needsOpencodeResolution) {
|
|
249
242
|
const catalog = loadOpencodeCatalog();
|
|
250
243
|
const { resolved, warnings: modelWarnings } = resolveOpencodeModels(models, {
|
|
251
244
|
catalog,
|
|
252
245
|
explicitProvider: opts.opencodeProvider,
|
|
253
246
|
});
|
|
254
|
-
opencodeModels =
|
|
247
|
+
opencodeModels =
|
|
248
|
+
previousMaps.opencodeModels !== undefined && !opts.opencodeProvider
|
|
249
|
+
? { ...previousMaps.opencodeModels }
|
|
250
|
+
: resolved;
|
|
251
|
+
for (const role of legacyOverrideRoles)
|
|
252
|
+
opencodeModels[role] = resolved[role];
|
|
255
253
|
for (const w of modelWarnings) {
|
|
256
254
|
warnings.push(`Warning: ${w}`);
|
|
257
255
|
}
|
|
@@ -263,11 +261,15 @@ export async function resolveInitInputs(params) {
|
|
|
263
261
|
opencodeClassModels = {};
|
|
264
262
|
for (const modelClass of MODEL_CLASSES) {
|
|
265
263
|
const alias = CLASS_MODELS[modelClass];
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
264
|
+
const recordedClass = previousMaps.opencodeClassModels !== undefined &&
|
|
265
|
+
!opts.opencodeProvider;
|
|
266
|
+
const resolvedModel = recordedClass
|
|
267
|
+
? previousMaps.opencodeClassModels?.[modelClass]
|
|
268
|
+
: providerResult.provider
|
|
269
|
+
? resolveAlias(providerResult.provider, alias, catalog)
|
|
270
|
+
: undefined;
|
|
269
271
|
opencodeClassModels[modelClass] = resolvedModel;
|
|
270
|
-
if (resolvedModel !== undefined)
|
|
272
|
+
if (resolvedModel !== undefined || recordedClass)
|
|
271
273
|
continue;
|
|
272
274
|
// One warning per unresolved model class: without it, every
|
|
273
275
|
// effort-tier variant keyed to this class is silently skipped
|
|
@@ -287,6 +289,18 @@ export async function resolveInitInputs(params) {
|
|
|
287
289
|
}
|
|
288
290
|
}
|
|
289
291
|
}
|
|
292
|
+
const routing = normalizeRoutingState({
|
|
293
|
+
harnesses,
|
|
294
|
+
models,
|
|
295
|
+
opencodeModels,
|
|
296
|
+
opencodeClassModels,
|
|
297
|
+
previousRouting,
|
|
298
|
+
routing: routingPatch,
|
|
299
|
+
legacyOverrideRoles,
|
|
300
|
+
// Changing provider is an explicit request to resolve all opencode leaves.
|
|
301
|
+
updateOpencodeModels: opts.opencodeProvider !== undefined,
|
|
302
|
+
updateOpencodeClassModels: opts.opencodeProvider !== undefined,
|
|
303
|
+
});
|
|
290
304
|
return {
|
|
291
305
|
harnesses,
|
|
292
306
|
profile,
|
|
@@ -294,6 +308,7 @@ export async function resolveInitInputs(params) {
|
|
|
294
308
|
tiers,
|
|
295
309
|
opencodeModels,
|
|
296
310
|
opencodeClassModels,
|
|
311
|
+
routing,
|
|
297
312
|
warnings,
|
|
298
313
|
};
|
|
299
314
|
}
|
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,8 @@ import { resolveInitInputs } from "./cli-inputs.js";
|
|
|
10
10
|
import { HARNESSES, detectHarnesses } from "./detect.js";
|
|
11
11
|
import { DEFAULT_MODELS, PROFILES } from "./models.js";
|
|
12
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";
|
|
13
15
|
import { OPERATOR_MANIFEST_FILENAME, OperatorManifestLockTimeoutError, applyRegistrationFailureMessage, createOperatorManifest, operatorManifestState, readOperatorManifest, resolveOperatorHome, safeRealpath, updateOperatorManifest, upsertOperatorTarget, } from "./operator-manifest.js";
|
|
14
16
|
import { runUninstall } from "./uninstall.js";
|
|
15
17
|
import { adoptExitCodeForStatus, adoptJsonExtras, inspectTarget, runDoctor, statOrClassify, suppressSuccessLine, targetReportToJson, } from "./doctor.js";
|
|
@@ -43,6 +45,20 @@ function requireDirectory(dir) {
|
|
|
43
45
|
}
|
|
44
46
|
return targetDir;
|
|
45
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
|
+
}
|
|
46
62
|
/**
|
|
47
63
|
* `resolveInitInputs` was extracted from `init` and typed against `init`'s
|
|
48
64
|
* per-repo `Manifest` (kit/version/files/installedAt included), since that
|
|
@@ -64,6 +80,8 @@ function defaultsAsManifest(defaults) {
|
|
|
64
80
|
models: { ...DEFAULT_MODELS, ...defaults.models },
|
|
65
81
|
profile: defaults.profile,
|
|
66
82
|
tiers: defaults.tiers,
|
|
83
|
+
routing: defaults.routing,
|
|
84
|
+
...parseOpencodeModelMaps(defaults),
|
|
67
85
|
files: {},
|
|
68
86
|
installedAt: "",
|
|
69
87
|
};
|
|
@@ -80,6 +98,8 @@ function defaultsEqual(a, b) {
|
|
|
80
98
|
profile: d.profile,
|
|
81
99
|
tiers: d.tiers,
|
|
82
100
|
models: Object.fromEntries(Object.entries(d.models).sort(([x], [y]) => x.localeCompare(y))),
|
|
101
|
+
routing: d.routing ?? {},
|
|
102
|
+
...parseOpencodeModelMaps(d),
|
|
83
103
|
});
|
|
84
104
|
return normalize(a) === normalize(b);
|
|
85
105
|
}
|
|
@@ -96,6 +116,8 @@ program
|
|
|
96
116
|
.option("-f, --force", "overwrite kit-owned files that have local edits")
|
|
97
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`)
|
|
98
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")
|
|
99
121
|
.option("--profile <profile>", `subagent role profile (${PROFILES.join(", ")}); default: full, or the previously installed profile on a re-run`)
|
|
100
122
|
.option("--opencode-provider <id>", "opencode provider id for alias resolution (e.g. github-copilot); auto-detected when omitted")
|
|
101
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")
|
|
@@ -105,6 +127,19 @@ program
|
|
|
105
127
|
if (!targetDir)
|
|
106
128
|
return;
|
|
107
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
|
+
}
|
|
108
143
|
// Say where files will land BEFORE anything is written; an accidental
|
|
109
144
|
// cwd (e.g. $HOME) is the most likely operator mistake.
|
|
110
145
|
console.log(`Installing into ${targetDir}`);
|
|
@@ -125,13 +160,16 @@ program
|
|
|
125
160
|
: "none recorded";
|
|
126
161
|
console.log(`Found existing install (${version.startsWith("unknown") ? version : `v${version}`}, harnesses: ${installedFor}, profile: ${previous.profile}, tiers: ${previous.tiers})`);
|
|
127
162
|
}
|
|
128
|
-
const { harnesses, profile, models, tiers, opencodeModels, opencodeClassModels, warnings, } = await resolveInitInputs(
|
|
163
|
+
const { harnesses, profile, models, tiers, opencodeModels, opencodeClassModels, routing: resolvedRouting, warnings, } = await resolveInitInputs(
|
|
129
164
|
// The sticky-branch wiring itself (neither `stickyPreChecked` nor
|
|
130
165
|
// `stickyAnnotateDetected` overridden, so `resolveInitInputs`'s own
|
|
131
166
|
// `?? []` / `?? detected` defaults apply) is pinned inside
|
|
132
167
|
// `buildInitInitInputs` and covered by a dedicated test
|
|
133
168
|
// (`test/cli-init.test.ts`), not by this call site.
|
|
134
|
-
buildInitInitInputs(detected, previous, interactive,
|
|
169
|
+
buildInitInitInputs(detected, previous, interactive, {
|
|
170
|
+
...opts,
|
|
171
|
+
routing,
|
|
172
|
+
}));
|
|
135
173
|
for (const w of warnings) {
|
|
136
174
|
process.stderr.write(`${w}\n`);
|
|
137
175
|
}
|
|
@@ -144,6 +182,9 @@ program
|
|
|
144
182
|
opencodeModels,
|
|
145
183
|
tiers,
|
|
146
184
|
opencodeClassModels,
|
|
185
|
+
routing: resolvedRouting,
|
|
186
|
+
routingMode: "replace",
|
|
187
|
+
codexCatalog,
|
|
147
188
|
});
|
|
148
189
|
showPaths("Created", report.written);
|
|
149
190
|
showPaths("Updated", report.updated);
|
|
@@ -159,6 +200,8 @@ program
|
|
|
159
200
|
.option("-y, --yes", "accept all defaults and skip prompts")
|
|
160
201
|
.option("--harness <list>", `comma-separated harnesses (${HARNESSES.join(", ")}); default: previously stored, or claude`)
|
|
161
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")
|
|
162
205
|
.option("--profile <profile>", `subagent role profile (${PROFILES.join(", ")}); default: full, or the previously stored profile on a re-run`)
|
|
163
206
|
.option("--opencode-provider <id>", "opencode provider id for alias resolution (e.g. github-copilot); auto-detected when omitted")
|
|
164
207
|
.option("--tiers", "select per-role effort-tier subagent variants by default; default: off, or the previously stored value on a re-run")
|
|
@@ -166,6 +209,19 @@ program
|
|
|
166
209
|
.action(async (opts) => {
|
|
167
210
|
const interactive = !opts.yes && isInteractive();
|
|
168
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
|
+
}
|
|
169
225
|
console.log(`Operator home: ${home}`);
|
|
170
226
|
const existing = readOperatorManifest(home);
|
|
171
227
|
if (existing) {
|
|
@@ -177,7 +233,7 @@ program
|
|
|
177
233
|
// No target repository exists to detect harnesses against; "claude"
|
|
178
234
|
// is the same shipped fallback `init` uses for a fresh install with
|
|
179
235
|
// nothing detected and nothing previously recorded.
|
|
180
|
-
const { harnesses, profile, models, tiers, warnings } = await resolveInitInputs({
|
|
236
|
+
const { harnesses, profile, models, tiers, routing, warnings, opencodeModels, opencodeClassModels, } = await resolveInitInputs({
|
|
181
237
|
// No target directory exists to detect against: the stored
|
|
182
238
|
// harnesses are the baseline, and only a first-ever setup falls
|
|
183
239
|
// back to claude, so a codex-only default is not widened silently.
|
|
@@ -186,16 +242,30 @@ program
|
|
|
186
242
|
: ["claude"],
|
|
187
243
|
interactive,
|
|
188
244
|
previous: defaultsAsManifest(existing?.defaults),
|
|
189
|
-
opts,
|
|
245
|
+
opts: { ...opts, routing: routingPatch },
|
|
190
246
|
});
|
|
191
247
|
for (const w of warnings) {
|
|
192
248
|
process.stderr.write(`${w}\n`);
|
|
193
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
|
+
}
|
|
194
262
|
const newDefaults = {
|
|
195
263
|
harnesses,
|
|
196
264
|
profile,
|
|
197
265
|
tiers,
|
|
198
266
|
models,
|
|
267
|
+
routing,
|
|
268
|
+
...legacyOpencodeFallbacks({ opencodeModels, opencodeClassModels }),
|
|
199
269
|
};
|
|
200
270
|
// The prompts and resolution above ran against `existing`, an
|
|
201
271
|
// unlocked read taken before this point; another process (an
|
|
@@ -410,6 +480,7 @@ function buildApplyPrevious(repoManifest, operatorDefaults, sync) {
|
|
|
410
480
|
const tiers = sync
|
|
411
481
|
? operatorDefaults.tiers
|
|
412
482
|
: (repoManifest?.tiers ?? operatorDefaults.tiers);
|
|
483
|
+
const { routing, ...compatibility } = mergeRoutingStateLayers(operatorDefaults, ...(!sync && repoManifest ? [repoManifest] : []));
|
|
413
484
|
return {
|
|
414
485
|
kit: "orchestrator-workflow",
|
|
415
486
|
version: PACKAGE_VERSION,
|
|
@@ -424,6 +495,8 @@ function buildApplyPrevious(repoManifest, operatorDefaults, sync) {
|
|
|
424
495
|
models,
|
|
425
496
|
profile,
|
|
426
497
|
tiers,
|
|
498
|
+
...(routing !== undefined ? { routing } : {}),
|
|
499
|
+
...compatibility,
|
|
427
500
|
files: {},
|
|
428
501
|
installedAt: "",
|
|
429
502
|
};
|
|
@@ -436,6 +509,8 @@ program
|
|
|
436
509
|
.option("-f, --force", "overwrite kit-owned files that have local edits")
|
|
437
510
|
.option("--harness <list>", `comma-separated harnesses (${HARNESSES.join(", ")}); default: the target's recorded harnesses, else the operator defaults, else detected`)
|
|
438
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")
|
|
439
514
|
.option("--profile <profile>", `subagent role profile (${PROFILES.join(", ")}); default: the target's recorded profile, else the operator default`)
|
|
440
515
|
.option("--opencode-provider <id>", "opencode provider id for alias resolution (e.g. github-copilot); auto-detected when omitted")
|
|
441
516
|
.option("--tiers", "also render per-role effort-tier subagent variants (<role>-<tier>.md); default: the target's recorded value, else the operator default")
|
|
@@ -445,6 +520,19 @@ program
|
|
|
445
520
|
.option("--pin <version>", "set or replace the target's recorded kit-version pin and apply this operator install regardless of any existing pin")
|
|
446
521
|
.option("--unpin", "clear the target's recorded kit-version pin and apply this operator install")
|
|
447
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
|
+
}
|
|
448
536
|
// --pin and --unpin express opposite intents (set a pin vs clear it);
|
|
449
537
|
// accepting both silently would make the effective pin depend on
|
|
450
538
|
// internal option-resolution order, so this is a usage error rather
|
|
@@ -536,7 +624,7 @@ program
|
|
|
536
624
|
}
|
|
537
625
|
const chosenHarnesses = resolveApplyHarnesses(targetDir, repoManifest, operatorManifest.defaults);
|
|
538
626
|
const previous = buildApplyPrevious(repoManifest, operatorManifest.defaults, Boolean(opts.sync));
|
|
539
|
-
const { harnesses, profile, models, tiers, opencodeModels, opencodeClassModels, warnings, } = await resolveInitInputs(
|
|
627
|
+
const { harnesses, profile, models, tiers, opencodeModels, opencodeClassModels, routing: resolvedRouting, warnings, } = await resolveInitInputs(
|
|
540
628
|
// `previous` is always defined here (`buildApplyPrevious` returns a
|
|
541
629
|
// synthetic object even for a target with no manifest of its own),
|
|
542
630
|
// so `previousIsRecordedManifest` cannot be `Boolean(previous)`; it
|
|
@@ -557,7 +645,10 @@ program
|
|
|
557
645
|
// `stickyAnnotateDetected`) is pinned inside `buildApplyInitInputs`
|
|
558
646
|
// rather than inlined here (agent-tasks fe834823, fix round 3,
|
|
559
647
|
// review finding 1).
|
|
560
|
-
buildApplyInitInputs(targetDir, chosenHarnesses, previous, interactive,
|
|
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)));
|
|
561
652
|
for (const w of warnings) {
|
|
562
653
|
process.stderr.write(`${w}\n`);
|
|
563
654
|
}
|
|
@@ -588,6 +679,9 @@ program
|
|
|
588
679
|
opencodeModels,
|
|
589
680
|
tiers,
|
|
590
681
|
opencodeClassModels,
|
|
682
|
+
routing: resolvedRouting,
|
|
683
|
+
routingMode: "replace",
|
|
684
|
+
codexCatalog,
|
|
591
685
|
pin,
|
|
592
686
|
});
|
|
593
687
|
showPaths("Created", report.written);
|
|
@@ -786,6 +880,12 @@ function printTargetDetail(target, operatorVersion) {
|
|
|
786
880
|
if (target.divergence.models) {
|
|
787
881
|
console.log(` models: ${target.divergentModelRoles.join(", ")}`);
|
|
788
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}`);
|
|
789
889
|
}
|
|
790
890
|
const showsVersionLagDetail = (target.status === "version-lag" ||
|
|
791
891
|
((target.status === "divergent" || target.status === "drift") &&
|
|
@@ -823,6 +923,10 @@ function operatorDefaultsFromRepoManifest(repoManifest) {
|
|
|
823
923
|
profile: repoManifest.profile,
|
|
824
924
|
tiers: repoManifest.tiers,
|
|
825
925
|
models: { ...repoManifest.models },
|
|
926
|
+
...parseOpencodeModelMaps(repoManifest),
|
|
927
|
+
...(repoManifest.routing !== undefined
|
|
928
|
+
? { routing: repoManifest.routing }
|
|
929
|
+
: {}),
|
|
826
930
|
};
|
|
827
931
|
}
|
|
828
932
|
/**
|
package/dist/codex.d.ts
ADDED
|
@@ -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
|
}
|