orchestrator-workflow 0.25.0 → 0.27.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 +205 -0
- package/INSTALL-AGENT.md +44 -4
- package/README.md +136 -4
- package/assets/agents/implementer.md +7 -0
- package/assets/agents-md-section.md +7 -3
- package/assets/skill/SKILL.md +56 -11
- package/assets/templates/00-goal.md +1 -0
- package/dist/cli-apply.d.ts +29 -0
- package/dist/cli-apply.js +29 -0
- package/dist/cli-inputs.d.ts +122 -0
- package/dist/cli-inputs.js +301 -0
- package/dist/cli.js +941 -183
- package/dist/detect.d.ts +11 -0
- package/dist/detect.js +30 -0
- package/dist/doctor.d.ts +222 -0
- package/dist/doctor.js +411 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/init.d.ts +35 -0
- package/dist/init.js +106 -6
- package/dist/operator-manifest.d.ts +277 -0
- package/dist/operator-manifest.js +538 -0
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ResolveInitInputsParams } from "./cli-inputs.js";
|
|
2
|
+
import type { Harness } from "./detect.js";
|
|
3
|
+
import type { Manifest } from "./init.js";
|
|
4
|
+
/** The subset of `apply`'s commander options that feed input resolution. */
|
|
5
|
+
export interface ApplyResolutionOptions {
|
|
6
|
+
harness?: string;
|
|
7
|
+
models?: string;
|
|
8
|
+
profile?: string;
|
|
9
|
+
opencodeProvider?: string;
|
|
10
|
+
tiers?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Builds `apply`'s own `resolveInitInputs` params, pinning the sticky-branch
|
|
14
|
+
* wiring so a future edit to the CLI action's call site cannot silently
|
|
15
|
+
* widen a deliberately templates-only target: `stickyPreChecked` is always
|
|
16
|
+
* a hardcoded `[]` here, never `chosenHarnesses` or `detected` (see
|
|
17
|
+
* `ResolveInitInputsParams.stickyPreChecked`'s doc comment for why).
|
|
18
|
+
* Kept in its own side-effect-free module (rather than inline in `cli.ts`,
|
|
19
|
+
* which runs `program.parseAsync(process.argv)` on import) so it can be
|
|
20
|
+
* unit-tested directly (`test/cli-apply.test.ts`) instead of only
|
|
21
|
+
* indirectly exercised through a spawned CLI process, and so a reversion
|
|
22
|
+
* here fails a targeted test instead of only the much larger
|
|
23
|
+
* interactive-prompt suite (agent-tasks fe834823, fix round 3, review
|
|
24
|
+
* finding 1). `stickyAnnotateDetected` is a fresh `detectHarnesses(targetDir)`
|
|
25
|
+
* call, independent of `chosenHarnesses`: it only feeds the checkbox's
|
|
26
|
+
* " (detected)" label (`ResolveInitInputsParams.stickyAnnotateDetected`),
|
|
27
|
+
* never the pre-check itself.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildApplyInitInputs(targetDir: string, chosenHarnesses: Harness[], previous: Manifest, interactive: boolean, opts: ApplyResolutionOptions, previousIsRecordedManifest: boolean): ResolveInitInputsParams;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { detectHarnesses } from "./detect.js";
|
|
2
|
+
/**
|
|
3
|
+
* Builds `apply`'s own `resolveInitInputs` params, pinning the sticky-branch
|
|
4
|
+
* wiring so a future edit to the CLI action's call site cannot silently
|
|
5
|
+
* widen a deliberately templates-only target: `stickyPreChecked` is always
|
|
6
|
+
* a hardcoded `[]` here, never `chosenHarnesses` or `detected` (see
|
|
7
|
+
* `ResolveInitInputsParams.stickyPreChecked`'s doc comment for why).
|
|
8
|
+
* Kept in its own side-effect-free module (rather than inline in `cli.ts`,
|
|
9
|
+
* which runs `program.parseAsync(process.argv)` on import) so it can be
|
|
10
|
+
* unit-tested directly (`test/cli-apply.test.ts`) instead of only
|
|
11
|
+
* indirectly exercised through a spawned CLI process, and so a reversion
|
|
12
|
+
* here fails a targeted test instead of only the much larger
|
|
13
|
+
* interactive-prompt suite (agent-tasks fe834823, fix round 3, review
|
|
14
|
+
* finding 1). `stickyAnnotateDetected` is a fresh `detectHarnesses(targetDir)`
|
|
15
|
+
* call, independent of `chosenHarnesses`: it only feeds the checkbox's
|
|
16
|
+
* " (detected)" label (`ResolveInitInputsParams.stickyAnnotateDetected`),
|
|
17
|
+
* never the pre-check itself.
|
|
18
|
+
*/
|
|
19
|
+
export function buildApplyInitInputs(targetDir, chosenHarnesses, previous, interactive, opts, previousIsRecordedManifest) {
|
|
20
|
+
return {
|
|
21
|
+
detected: chosenHarnesses,
|
|
22
|
+
stickyPreChecked: [],
|
|
23
|
+
stickyAnnotateDetected: detectHarnesses(targetDir),
|
|
24
|
+
interactive,
|
|
25
|
+
previous,
|
|
26
|
+
opts,
|
|
27
|
+
previousIsRecordedManifest,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type { Harness } from "./detect.js";
|
|
2
|
+
import type { Manifest } from "./init.js";
|
|
3
|
+
import type { ModelClass, Profile, Role } from "./models.js";
|
|
4
|
+
export declare function promptHarnesses(detected: Harness[], installed: Harness[], fallbackToClaude?: boolean, annotateDetected?: Harness[]): Promise<Harness[]>;
|
|
5
|
+
export declare function promptProfile(base: Profile): Promise<Profile>;
|
|
6
|
+
export declare function promptModels(base: Record<Role, string>, roles: Role[]): Promise<Record<Role, string>>;
|
|
7
|
+
/** The subset of `init`'s commander options that feed input resolution. */
|
|
8
|
+
export interface InitResolutionOptions {
|
|
9
|
+
harness?: string;
|
|
10
|
+
models?: string;
|
|
11
|
+
profile?: string;
|
|
12
|
+
opencodeProvider?: string;
|
|
13
|
+
tiers?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface ResolveInitInputsParams {
|
|
16
|
+
/** Result of `detectHarnesses(targetDir)`; passed in so the caller can
|
|
17
|
+
* print it before resolution starts, matching `init`'s existing output
|
|
18
|
+
* order, without this function reading the filesystem a second time. */
|
|
19
|
+
detected: Harness[];
|
|
20
|
+
interactive: boolean;
|
|
21
|
+
/** The previously installed manifest, if any (`readInstalledManifest`). */
|
|
22
|
+
previous: Manifest | undefined;
|
|
23
|
+
opts: InitResolutionOptions;
|
|
24
|
+
/**
|
|
25
|
+
* True when `previous` is backed by the target's own actually-recorded
|
|
26
|
+
* manifest, as opposed to a wholly synthetic object with no repo
|
|
27
|
+
* manifest behind it at all. `init` sets this whenever it has a
|
|
28
|
+
* `previous` (`readInstalledManifest(targetDir)` returned one). `apply`
|
|
29
|
+
* always hands `resolveInitInputs` a non-`undefined` `previous` (its
|
|
30
|
+
* `buildApplyPrevious` synthesizes one even for a target with no
|
|
31
|
+
* manifest of its own, to carry the operator-defaults floor), so it sets
|
|
32
|
+
* this flag from whether the target actually has a repo manifest
|
|
33
|
+
* (`Boolean(repoManifest)`), not from whether `previous` itself is
|
|
34
|
+
* defined. Only consulted for the harnesses-stickiness rule below,
|
|
35
|
+
* together with `previous.harnessesRecordedEmpty` (which `apply`'s
|
|
36
|
+
* `buildApplyPrevious` carries straight through from that repo
|
|
37
|
+
* manifest): a real recorded `harnesses: []` means a deliberate
|
|
38
|
+
* `--harness none` install, and a plain re-run (init or apply, no
|
|
39
|
+
* `--harness` flag) must not silently widen it via detection or the
|
|
40
|
+
* operator manifest's default harnesses. This flag alone does not
|
|
41
|
+
* distinguish a deliberate `harnesses: []` from a damaged/legacy
|
|
42
|
+
* manifest whose raw `harnesses` field was missing, malformed, or an
|
|
43
|
+
* array whose every entry failed the known-harness filter (all of which
|
|
44
|
+
* also sanitize to `harnesses: []`) -- that distinction is
|
|
45
|
+
* `harnessesRecordedEmpty`'s job; both must hold for the stickiness gate
|
|
46
|
+
* to fire, so a target with no repo manifest at all, or one with a
|
|
47
|
+
* missing/malformed `harnesses` field, still falls through to the
|
|
48
|
+
* fallback chain below unchanged.
|
|
49
|
+
*/
|
|
50
|
+
previousIsRecordedManifest?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The entries pre-checked in the interactive prompt when the target
|
|
53
|
+
* recorded `harnesses: []` (the harnesses-stickiness gate's branch,
|
|
54
|
+
* gated on `previousIsRecordedManifest && previous.
|
|
55
|
+
* harnessesRecordedEmpty`). Defaults to `detected` when omitted, which
|
|
56
|
+
* is `init`'s own call site's behaviour (it does not pass this field at
|
|
57
|
+
* all): a fresh interactive re-run on a templates-only `init` target
|
|
58
|
+
* still pre-checks whatever `detectHarnesses(targetDir)` finds on disk,
|
|
59
|
+
* unchanged from before this field existed. `apply`'s call site passes
|
|
60
|
+
* `[]` instead: the operator's recorded `harnesses: []` is the intent
|
|
61
|
+
* that matters, not a `.claude/`-style directory the harness itself
|
|
62
|
+
* left on disk, which is a weak signal and must not re-widen a
|
|
63
|
+
* deliberate `--harness none` install just because a bare Enter is
|
|
64
|
+
* pressed (agent-tasks fe834823). Only the sticky branch reads this
|
|
65
|
+
* field; the normal (non-recorded-empty) branch still prompts from
|
|
66
|
+
* `detected` unchanged, matching `apply`'s existing pre-check behaviour
|
|
67
|
+
* on a normal target.
|
|
68
|
+
*/
|
|
69
|
+
stickyPreChecked?: Harness[];
|
|
70
|
+
/**
|
|
71
|
+
* The sticky branch's own `promptHarnesses` " (detected)" label source,
|
|
72
|
+
* independent of `stickyPreChecked` (which drives what is actually
|
|
73
|
+
* pre-checked, not what is merely labelled). Defaults to
|
|
74
|
+
* `stickyPreChecked ?? detected` when omitted, matching `promptHarnesses`'
|
|
75
|
+
* own default and `init`'s call site (which omits both fields, so its
|
|
76
|
+
* sticky prompt still labels from real on-disk detection, unchanged).
|
|
77
|
+
* `apply`'s call site passes `[]` for `stickyPreChecked` (nothing is
|
|
78
|
+
* pre-checked; see that field's doc comment) but still wants the
|
|
79
|
+
* operator to see which harness is actually on disk, so it passes a
|
|
80
|
+
* fresh `detectHarnesses(targetDir)` call here instead: labelling is a
|
|
81
|
+
* hint, not an intent signal, so it is safe to annotate what the
|
|
82
|
+
* pre-check itself must not read (agent-tasks fe834823, fix round 3).
|
|
83
|
+
*/
|
|
84
|
+
stickyAnnotateDetected?: Harness[];
|
|
85
|
+
}
|
|
86
|
+
export interface ResolvedInitInputs {
|
|
87
|
+
harnesses: Harness[];
|
|
88
|
+
profile: Profile;
|
|
89
|
+
models: Record<Role, string>;
|
|
90
|
+
tiers: boolean;
|
|
91
|
+
opencodeModels?: Record<Role, string | undefined>;
|
|
92
|
+
opencodeClassModels?: Record<ModelClass, string | undefined>;
|
|
93
|
+
/**
|
|
94
|
+
* Warning lines to print, in order, exactly as `init` printed them to
|
|
95
|
+
* stderr before this extraction (each written as `${line}\n`). Returned
|
|
96
|
+
* as data rather than printed here so the caller decides where/whether to
|
|
97
|
+
* print them.
|
|
98
|
+
*/
|
|
99
|
+
warnings: string[];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Resolves everything `runInit` needs (harnesses, profile, models, tiers,
|
|
103
|
+
* the opencode model resolutions) from the CLI-parsed options, the target
|
|
104
|
+
* directory, whether the session is interactive, and the previously
|
|
105
|
+
* installed manifest. Used by `init`'s action today, and reusable by a
|
|
106
|
+
* later `apply --target` command without duplicating this logic.
|
|
107
|
+
*
|
|
108
|
+
* Every override-vs-persist rule below matches `init`'s pre-extraction
|
|
109
|
+
* behaviour: an explicit flag always overrides; a plain re-run (flag
|
|
110
|
+
* omitted) keeps the previously installed value; a fresh install with no
|
|
111
|
+
* prior manifest falls back to the shipped default.
|
|
112
|
+
*
|
|
113
|
+
* `params.detected` doubles as the fallback-chain input the non-sticky
|
|
114
|
+
* "else" branch below prompts and falls back from, and (for `init`'s call
|
|
115
|
+
* site only, since it omits `stickyPreChecked`) the harnesses-stickiness
|
|
116
|
+
* branch's own pre-check. `apply`'s call site's `detected` is
|
|
117
|
+
* `resolveApplyHarnesses`'s fallback-chain result (never empty), which is
|
|
118
|
+
* not what the sticky branch should pre-check (see
|
|
119
|
+
* `ResolveInitInputsParams.stickyPreChecked`'s doc comment), so it passes
|
|
120
|
+
* that field separately (`[]`) for the sticky branch to read instead.
|
|
121
|
+
*/
|
|
122
|
+
export declare function resolveInitInputs(params: ResolveInitInputsParams): Promise<ResolvedInitInputs>;
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import inquirer from "inquirer";
|
|
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";
|
|
4
|
+
import { detectProvider, loadOpencodeCatalog, resolveAlias, resolveOpencodeModels, } from "./opencode.js";
|
|
5
|
+
export async function promptHarnesses(detected, installed, fallbackToClaude = true,
|
|
6
|
+
// Drives only the checkbox's " (detected)" label suffix, independent of
|
|
7
|
+
// `detected`'s own role in pre-checking a choice: defaults to `detected`
|
|
8
|
+
// so every call site that omits this parameter keeps annotating exactly
|
|
9
|
+
// what it pre-checks from, unchanged. `apply`'s sticky-branch call site
|
|
10
|
+
// is the one caller that passes a different value here: it pre-checks
|
|
11
|
+
// nothing (`stickyPreChecked ?? detected` is `[]`) but still wants the
|
|
12
|
+
// operator to see which harness is actually on disk, so it annotates
|
|
13
|
+
// from a fresh `detectHarnesses(targetDir)` call instead
|
|
14
|
+
// (agent-tasks fe834823, fix round 3).
|
|
15
|
+
annotateDetected = detected) {
|
|
16
|
+
const known = [...new Set([...detected, ...installed])];
|
|
17
|
+
// Nothing detected and nothing previously installed: the plain-first-run
|
|
18
|
+
// case pre-checks `claude` as a sane default (`fallbackToClaude`'s default
|
|
19
|
+
// `true`). The templates-only re-run branch below opts OUT of that
|
|
20
|
+
// (`fallbackToClaude: false`): a repo the operator explicitly recorded as
|
|
21
|
+
// `harnesses: []` has no harness files by construction, so `detected` is
|
|
22
|
+
// always empty there too, and pre-checking `claude` on Enter would
|
|
23
|
+
// silently re-widen an explicit `--harness none` install -- contradicting
|
|
24
|
+
// README.md's and this function's own "nothing forced pre-selected" claim
|
|
25
|
+
// (see CHANGELOG). That "`detected` is always empty there too" premise
|
|
26
|
+
// holds for `init`'s own call site, where `detected` is
|
|
27
|
+
// `detectHarnesses(targetDir)` on a target with no harness files by
|
|
28
|
+
// construction. `apply`'s call site (`resolveInitInputs`'s interactive
|
|
29
|
+
// branch just below) does not call this function with its own
|
|
30
|
+
// `resolveApplyHarnesses` result at all for this branch: that result is
|
|
31
|
+
// never empty (it falls back through the operator default, then
|
|
32
|
+
// detection, then `["claude"]`), so it passes `stickyPreChecked: []`
|
|
33
|
+
// here instead, regardless of what is actually on disk -- the operator's
|
|
34
|
+
// recorded `harnesses: []` is the intent that matters, not a harness
|
|
35
|
+
// config a harness itself left behind (agent-tasks fe834823; the
|
|
36
|
+
// residual noted in docs/okf/log.md's 2026-08-31 entry is closed).
|
|
37
|
+
const preselected = known.length > 0 ? known : fallbackToClaude ? ["claude"] : [];
|
|
38
|
+
const { harnesses } = await inquirer.prompt([
|
|
39
|
+
{
|
|
40
|
+
type: "checkbox",
|
|
41
|
+
name: "harnesses",
|
|
42
|
+
message: "Install adapters for which harnesses? (deselect all for templates only, no harness)",
|
|
43
|
+
choices: HARNESSES.map((harness) => ({
|
|
44
|
+
name: harness + (annotateDetected.includes(harness) ? " (detected)" : ""),
|
|
45
|
+
value: harness,
|
|
46
|
+
checked: preselected.includes(harness),
|
|
47
|
+
})),
|
|
48
|
+
// An empty selection is a supported state
|
|
49
|
+
// (`--harness none`, templates-only mode): it used to be rejected
|
|
50
|
+
// here because every install always wrote at least one harness
|
|
51
|
+
// adapter; there is no longer a reason to require one.
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
return harnesses;
|
|
55
|
+
}
|
|
56
|
+
export async function promptProfile(base) {
|
|
57
|
+
// Labels are derived from rolesForProfile so a future role addition (like
|
|
58
|
+
// the advisor role) shows up here automatically instead of silently
|
|
59
|
+
// falling out of sync with the roles the profile actually installs.
|
|
60
|
+
const fullRoles = rolesForProfile("full").join(", ");
|
|
61
|
+
const minimalRoles = rolesForProfile("minimal").join(", ");
|
|
62
|
+
const { profile } = await inquirer.prompt([
|
|
63
|
+
{
|
|
64
|
+
type: "list",
|
|
65
|
+
name: "profile",
|
|
66
|
+
message: "Which subagent roles should be installed?",
|
|
67
|
+
default: base,
|
|
68
|
+
choices: [
|
|
69
|
+
{
|
|
70
|
+
name: `full — ${fullRoles} (default)`,
|
|
71
|
+
value: "full",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: `minimal — ${minimalRoles} only (reviewer is never optional)`,
|
|
75
|
+
value: "minimal",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
]);
|
|
80
|
+
return profile;
|
|
81
|
+
}
|
|
82
|
+
export async function promptModels(base, roles) {
|
|
83
|
+
const models = { ...base };
|
|
84
|
+
for (const role of roles) {
|
|
85
|
+
const { choice } = await inquirer.prompt([
|
|
86
|
+
{
|
|
87
|
+
type: "list",
|
|
88
|
+
name: "choice",
|
|
89
|
+
message: `Model for the ${role} subagent:`,
|
|
90
|
+
default: models[role],
|
|
91
|
+
choices: [
|
|
92
|
+
...MODEL_ALIASES.map((alias) => ({
|
|
93
|
+
name: alias === DEFAULT_MODELS[role] ? `${alias} (default)` : alias,
|
|
94
|
+
value: alias,
|
|
95
|
+
})),
|
|
96
|
+
{ name: "custom model id", value: "__custom__" },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
if (choice === "__custom__") {
|
|
101
|
+
const { custom } = await inquirer.prompt([
|
|
102
|
+
{
|
|
103
|
+
type: "input",
|
|
104
|
+
name: "custom",
|
|
105
|
+
message: `Custom model id for ${role}:`,
|
|
106
|
+
validate: (value) => {
|
|
107
|
+
try {
|
|
108
|
+
assertValidModelId(value.trim());
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
return error instanceof Error ? error.message : String(error);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
models[role] = custom.trim();
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
models[role] = choice;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return models;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Resolves everything `runInit` needs (harnesses, profile, models, tiers,
|
|
127
|
+
* the opencode model resolutions) from the CLI-parsed options, the target
|
|
128
|
+
* directory, whether the session is interactive, and the previously
|
|
129
|
+
* installed manifest. Used by `init`'s action today, and reusable by a
|
|
130
|
+
* later `apply --target` command without duplicating this logic.
|
|
131
|
+
*
|
|
132
|
+
* Every override-vs-persist rule below matches `init`'s pre-extraction
|
|
133
|
+
* behaviour: an explicit flag always overrides; a plain re-run (flag
|
|
134
|
+
* omitted) keeps the previously installed value; a fresh install with no
|
|
135
|
+
* prior manifest falls back to the shipped default.
|
|
136
|
+
*
|
|
137
|
+
* `params.detected` doubles as the fallback-chain input the non-sticky
|
|
138
|
+
* "else" branch below prompts and falls back from, and (for `init`'s call
|
|
139
|
+
* site only, since it omits `stickyPreChecked`) the harnesses-stickiness
|
|
140
|
+
* branch's own pre-check. `apply`'s call site's `detected` is
|
|
141
|
+
* `resolveApplyHarnesses`'s fallback-chain result (never empty), which is
|
|
142
|
+
* not what the sticky branch should pre-check (see
|
|
143
|
+
* `ResolveInitInputsParams.stickyPreChecked`'s doc comment), so it passes
|
|
144
|
+
* that field separately (`[]`) for the sticky branch to read instead.
|
|
145
|
+
*/
|
|
146
|
+
export async function resolveInitInputs(params) {
|
|
147
|
+
const { detected, interactive, previous, opts, previousIsRecordedManifest, stickyPreChecked, stickyAnnotateDetected, } = params;
|
|
148
|
+
let harnesses;
|
|
149
|
+
if (opts.harness) {
|
|
150
|
+
harnesses = parseHarnessOption(opts.harness);
|
|
151
|
+
}
|
|
152
|
+
else if (previousIsRecordedManifest &&
|
|
153
|
+
previous &&
|
|
154
|
+
previous.harnessesRecordedEmpty) {
|
|
155
|
+
// A recorded previous manifest with harnesses: [] was an explicit
|
|
156
|
+
// --harness none (templates-only) install. A plain non-interactive
|
|
157
|
+
// re-run (no --harness flag) must stay templates-only rather than
|
|
158
|
+
// falling back to filesystem detection and silently installing a
|
|
159
|
+
// harness (e.g. claude) the operator never asked for; adding one back
|
|
160
|
+
// requires an explicit --harness on this run, the same
|
|
161
|
+
// override-vs-persist rule --profile/--models/--tiers already use,
|
|
162
|
+
// just applied to the "no harnesses" case specifically.
|
|
163
|
+
// `harnessesRecordedEmpty` gates this on the raw JSON's `harnesses`
|
|
164
|
+
// field having actually been an empty array: a missing/malformed field,
|
|
165
|
+
// or an array whose every entry failed the known-harness filter (e.g.
|
|
166
|
+
// ["cursor"], all-unknown names), also sanitizes to
|
|
167
|
+
// `harnesses.length === 0` (readInstalledManifest in init.ts) but must
|
|
168
|
+
// fall through to detection below instead, the same as any other
|
|
169
|
+
// damaged manifest (see CHANGELOG).
|
|
170
|
+
//
|
|
171
|
+
// An interactive re-run is different: stickiness only protects a
|
|
172
|
+
// non-interactive call (`--yes`, or any other flow with no prompt) from
|
|
173
|
+
// silently widening an explicit "none" back out; an interactive session
|
|
174
|
+
// can already ask and let the operator decide, so it still prompts here
|
|
175
|
+
// instead of skipping straight to templates-only. `installed` is passed
|
|
176
|
+
// as `[]` (not the recorded `previous.harnesses`) so nothing is
|
|
177
|
+
// pre-checked from the previous install, unlike the "else" branch
|
|
178
|
+
// below's normal re-run prompt.
|
|
179
|
+
// `stickyPreChecked ?? detected` is used here rather than plain
|
|
180
|
+
// `detected`: `init` does not pass `stickyPreChecked` at all, so its
|
|
181
|
+
// own call site keeps pre-checking whatever `detectHarnesses(targetDir)`
|
|
182
|
+
// finds on disk, unchanged from before this field existed. `apply`
|
|
183
|
+
// passes `stickyPreChecked: []`: the operator's recorded
|
|
184
|
+
// `harnesses: []` is the intent that matters here, not a harness
|
|
185
|
+
// config left on disk (a weak signal `apply`'s own `detected` --
|
|
186
|
+
// `resolveApplyHarnesses`'s fallback-chain result, never empty --
|
|
187
|
+
// cannot represent either), so the interactive prompt on a templates-
|
|
188
|
+
// only `apply` target starts with nothing pre-checked at all
|
|
189
|
+
// (agent-tasks fe834823). `fallbackToClaude: false` closes the same
|
|
190
|
+
// gap for the case where nothing is pre-checked either: without it,
|
|
191
|
+
// `promptHarnesses` would pre-check `claude` on its own "nothing
|
|
192
|
+
// known" fallback, re-widening the install on a bare Enter.
|
|
193
|
+
// `stickyAnnotateDetected` is passed through as the fourth argument so
|
|
194
|
+
// the checkbox's " (detected)" label can still point at what is
|
|
195
|
+
// actually on disk even though nothing is pre-checked from it; when
|
|
196
|
+
// omitted (as `init`'s call site does), `promptHarnesses` defaults it
|
|
197
|
+
// to its own first argument, i.e. `stickyPreChecked ?? detected`,
|
|
198
|
+
// matching pre-round-3 labelling behaviour exactly.
|
|
199
|
+
harnesses = interactive
|
|
200
|
+
? await promptHarnesses(stickyPreChecked ?? detected, [], false, stickyAnnotateDetected)
|
|
201
|
+
: [];
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
const installed = previous?.harnesses ?? [];
|
|
205
|
+
const fallback = [...new Set([...detected, ...installed])];
|
|
206
|
+
harnesses = interactive
|
|
207
|
+
? await promptHarnesses(detected, installed)
|
|
208
|
+
: fallback.length > 0
|
|
209
|
+
? fallback
|
|
210
|
+
: ["claude"];
|
|
211
|
+
}
|
|
212
|
+
// Explicit --profile always overrides; a plain re-run keeps the
|
|
213
|
+
// profile from the previous install (same override-vs-persist rule as
|
|
214
|
+
// --harness/--models above); a fresh install with no prior manifest
|
|
215
|
+
// defaults to full.
|
|
216
|
+
let profile;
|
|
217
|
+
if (opts.profile) {
|
|
218
|
+
profile = parseProfile(opts.profile);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
profile = previous?.profile ?? DEFAULT_PROFILE;
|
|
222
|
+
if (interactive)
|
|
223
|
+
profile = await promptProfile(profile);
|
|
224
|
+
}
|
|
225
|
+
let models = {
|
|
226
|
+
...DEFAULT_MODELS,
|
|
227
|
+
...(previous?.models ?? {}),
|
|
228
|
+
};
|
|
229
|
+
if (opts.models)
|
|
230
|
+
models = parseModelsSpec(opts.models, models);
|
|
231
|
+
if (interactive && !opts.models)
|
|
232
|
+
models = await promptModels(models, rolesForProfile(profile));
|
|
233
|
+
// Explicit --tiers/--no-tiers always override; a plain re-run (neither
|
|
234
|
+
// flag passed) keeps whatever the previous install had (default false
|
|
235
|
+
// for a fresh install), same override-vs-persist rule as
|
|
236
|
+
// --profile/--models above. commander's negatable-option pairing
|
|
237
|
+
// (--tiers / --no-tiers declared under the same "tiers" option name)
|
|
238
|
+
// resolves opts.tiers to `true` when --tiers is passed, `false` when
|
|
239
|
+
// --no-tiers is passed, and `undefined` when neither is passed; the
|
|
240
|
+
// CLI re-run test verifies this against the installed commander
|
|
241
|
+
// version rather than assuming it. No interactive prompt: tiers is
|
|
242
|
+
// opt-in/off via the flags only.
|
|
243
|
+
const tiers = opts.tiers ?? previous?.tiers ?? false;
|
|
244
|
+
// Resolve opencode model aliases against the live catalog when the opencode
|
|
245
|
+
// harness is selected. The shell-out stays reachable only from this
|
|
246
|
+
// resolution step, keeping runInit pure.
|
|
247
|
+
let opencodeModels;
|
|
248
|
+
let opencodeClassModels;
|
|
249
|
+
const warnings = [];
|
|
250
|
+
if (harnesses.includes("opencode")) {
|
|
251
|
+
const catalog = loadOpencodeCatalog();
|
|
252
|
+
const { resolved, warnings: modelWarnings } = resolveOpencodeModels(models, {
|
|
253
|
+
catalog,
|
|
254
|
+
explicitProvider: opts.opencodeProvider,
|
|
255
|
+
});
|
|
256
|
+
opencodeModels = resolved;
|
|
257
|
+
for (const w of modelWarnings) {
|
|
258
|
+
warnings.push(`Warning: ${w}`);
|
|
259
|
+
}
|
|
260
|
+
if (tiers) {
|
|
261
|
+
const providerResult = detectProvider({
|
|
262
|
+
catalog,
|
|
263
|
+
explicit: opts.opencodeProvider,
|
|
264
|
+
});
|
|
265
|
+
opencodeClassModels = {};
|
|
266
|
+
for (const modelClass of MODEL_CLASSES) {
|
|
267
|
+
const alias = CLASS_MODELS[modelClass];
|
|
268
|
+
const resolvedModel = providerResult.provider
|
|
269
|
+
? resolveAlias(providerResult.provider, alias, catalog)
|
|
270
|
+
: undefined;
|
|
271
|
+
opencodeClassModels[modelClass] = resolvedModel;
|
|
272
|
+
if (resolvedModel !== undefined)
|
|
273
|
+
continue;
|
|
274
|
+
// One warning per unresolved model class: without it, every
|
|
275
|
+
// effort-tier variant keyed to this class is silently skipped
|
|
276
|
+
// (init.ts skips the variant write entirely when the class
|
|
277
|
+
// model is unresolved), with nothing on stderr saying why.
|
|
278
|
+
const reason = providerResult.provider
|
|
279
|
+
? `provider "${providerResult.provider}" has no "${alias}" model in the catalog`
|
|
280
|
+
: providerResult.ambiguous
|
|
281
|
+
? `multiple providers offer Claude models in the live catalog; cannot auto-detect`
|
|
282
|
+
: `no provider offering Claude models found in the live catalog`;
|
|
283
|
+
// States the real effect (no variant file at all, not just a
|
|
284
|
+
// missing model: line, since init.ts skips the write entirely
|
|
285
|
+
// when the class never resolves) and the real scope (opencode
|
|
286
|
+
// only: Claude Code variants resolve model: from a plain alias
|
|
287
|
+
// and need no live catalog lookup, so they are unaffected).
|
|
288
|
+
warnings.push(`Warning: Tier model class "${modelClass}" (alias "${alias}") could not be resolved to an opencode model id (${reason}); no opencode effort-tier variant files will be rendered for this class (Claude Code variants are unaffected).`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
harnesses,
|
|
294
|
+
profile,
|
|
295
|
+
models,
|
|
296
|
+
tiers,
|
|
297
|
+
opencodeModels,
|
|
298
|
+
opencodeClassModels,
|
|
299
|
+
warnings,
|
|
300
|
+
};
|
|
301
|
+
}
|