orchestrator-workflow 0.5.0 → 0.6.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/INSTALL-AGENT.md +27 -9
- package/README.md +20 -6
- package/dist/cli.js +18 -0
- package/dist/init.d.ts +8 -0
- package/dist/init.js +11 -3
- package/dist/models.d.ts +8 -1
- package/dist/models.js +8 -13
- package/dist/opencode.d.ts +66 -0
- package/dist/opencode.js +200 -0
- package/dist/uninstall.js +2 -0
- package/package.json +2 -2
package/INSTALL-AGENT.md
CHANGED
|
@@ -49,7 +49,8 @@ The install creates or touches only these paths:
|
|
|
49
49
|
- `.claude/skills/orchestrator-workflow/SKILL.md` and
|
|
50
50
|
`.claude/agents/{explorer,task-slicer,implementer,reviewer}.md` (Claude Code)
|
|
51
51
|
- `.agents/skills/orchestrator-workflow/SKILL.md` (Codex)
|
|
52
|
-
- `.opencode/
|
|
52
|
+
- `.opencode/skills/orchestrator-workflow/SKILL.md` and
|
|
53
|
+
`.opencode/agents/{explorer,task-slicer,implementer,reviewer}.md` (opencode)
|
|
53
54
|
|
|
54
55
|
Nothing else in the repository is modified. Locally edited files are
|
|
55
56
|
reported as conflicts and left alone, never overwritten silently; the
|
|
@@ -111,14 +112,31 @@ steps in the repository you were asked to install into.
|
|
|
111
112
|
line directly after the `model:` line. Ensure `CLAUDE.md` exists and
|
|
112
113
|
contains a line `@AGENTS.md`.
|
|
113
114
|
- Codex: `.agents/skills/orchestrator-workflow/SKILL.md`, same skill file.
|
|
114
|
-
- opencode: `.opencode/
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
`
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`
|
|
115
|
+
- opencode: `.opencode/skills/orchestrator-workflow/SKILL.md` from
|
|
116
|
+
`assets/skill/SKILL.md`, unchanged.
|
|
117
|
+
`.opencode/agents/<role>.md` from `assets/agents/<role>.md`, with the
|
|
118
|
+
frontmatter rewritten to this order: `description:` (unchanged), then
|
|
119
|
+
`mode: subagent`; the `name:` line is dropped. Only emit a
|
|
120
|
+
`model: <provider/model-id>` line when you have a fully-qualified id
|
|
121
|
+
(i.e. the value contains a `/`, such as
|
|
122
|
+
`github-copilot/claude-sonnet-4.6`). For a bare alias (`sonnet`, `opus`,
|
|
123
|
+
`haiku`) or any bare id without a provider prefix, **omit the `model:`
|
|
124
|
+
line entirely** — the subagent then inherits the session/default model,
|
|
125
|
+
which is the safe portable fallback. The installed CLI resolves aliases
|
|
126
|
+
to fully-qualified ids by running `opencode models` at install time; in a
|
|
127
|
+
manual install you may not have a live catalog, so omitting `model:` is
|
|
128
|
+
correct. For the explorer role additionally, `permission:` goes on a new
|
|
129
|
+
line directly after `mode: subagent` (or after `model:` when that line is
|
|
130
|
+
present), followed by ` edit: deny` (two-space indent) on the next line.
|
|
131
|
+
Example explorer frontmatter when no model is resolved:
|
|
132
|
+
```yaml
|
|
133
|
+
---
|
|
134
|
+
description: "..."
|
|
135
|
+
mode: subagent
|
|
136
|
+
permission:
|
|
137
|
+
edit: deny
|
|
138
|
+
---
|
|
139
|
+
```
|
|
122
140
|
- `.ai/workflow/manifest.json`, exactly this shape (harnesses MUST be an
|
|
123
141
|
array, models keyed by role, version = the kit version you installed):
|
|
124
142
|
|
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ Per selected harness:
|
|
|
99
99
|
|---|---|---|
|
|
100
100
|
| Claude Code | `.claude/skills/orchestrator-workflow/SKILL.md`, `.claude/agents/{explorer,task-slicer,implementer,reviewer}.md`, `CLAUDE.md` | Claude Code reads `CLAUDE.md`, not `AGENTS.md`; the installer adds an additive `@AGENTS.md` import. Subagent models go into the `model:` frontmatter; the read-only explorer also gets `disallowedTools: Edit, Write, NotebookEdit`. |
|
|
101
101
|
| OpenAI Codex | `.agents/skills/orchestrator-workflow/SKILL.md` | Codex reads `AGENTS.md` natively. There is no standardized project-level subagent definition; the skill instructs running the roles inline with the same contracts. |
|
|
102
|
-
| opencode | `.opencode/agents/{explorer,task-slicer,implementer,reviewer}.md` | opencode reads `AGENTS.md` natively
|
|
102
|
+
| opencode | `.opencode/skills/orchestrator-workflow/SKILL.md`, `.opencode/agents/{explorer,task-slicer,implementer,reviewer}.md` | opencode reads `AGENTS.md` natively. Subagents get `mode: subagent`; the explorer also gets `permission: edit: deny`. Model resolution is described below. |
|
|
103
103
|
|
|
104
104
|
## Model preselection
|
|
105
105
|
|
|
@@ -113,11 +113,25 @@ Each subagent role gets a model, chosen interactively or via `--models`:
|
|
|
113
113
|
| reviewer | `opus` | skeptical review benefits from the strongest model |
|
|
114
114
|
|
|
115
115
|
The orchestrator itself runs on the session's main model; use the strongest
|
|
116
|
-
reasoning model available.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
reasoning model available. The chosen mapping is recorded in
|
|
117
|
+
`.ai/workflow/manifest.json` and reused as the default on later re-runs.
|
|
118
|
+
|
|
119
|
+
**opencode model resolution.** opencode requires fully-qualified `provider/model-id`
|
|
120
|
+
strings (e.g. `github-copilot/claude-sonnet-4.6`). At install time the CLI
|
|
121
|
+
runs `opencode models` to fetch the live catalog and auto-detects which
|
|
122
|
+
provider to use (the one that offers Claude models). When exactly one such
|
|
123
|
+
provider exists the aliases are resolved to the highest-version matching id in
|
|
124
|
+
the catalog. When multiple providers offer Claude models the CLI warns and asks
|
|
125
|
+
you to pass `--opencode-provider <id>` to disambiguate, or to supply
|
|
126
|
+
fully-qualified ids per role via `--models`. If no resolution is possible
|
|
127
|
+
(catalog empty, `opencode` binary absent, ambiguous provider) the `model:`
|
|
128
|
+
frontmatter line is omitted entirely and the subagent inherits the
|
|
129
|
+
session/default model — a safe, portable fallback. Fully-qualified ids in
|
|
130
|
+
`--models` always pass through unchanged regardless of the catalog.
|
|
131
|
+
Nested-path providers like `openrouter` (whose ids look like
|
|
132
|
+
`openrouter/anthropic/claude-...`) are not auto-resolved from aliases and must
|
|
133
|
+
be supplied as a fully-qualified `--models` entry, e.g.
|
|
134
|
+
`reviewer=openrouter/anthropic/claude-opus-4.8`.
|
|
121
135
|
|
|
122
136
|
## Ownership and re-runs
|
|
123
137
|
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import inquirer from "inquirer";
|
|
|
6
6
|
import { PACKAGE_VERSION } from "./assets.js";
|
|
7
7
|
import { HARNESSES, detectHarnesses, parseHarnessList } from "./detect.js";
|
|
8
8
|
import { DEFAULT_MODELS, MODEL_ALIASES, ROLES, assertValidModelId, parseModelsSpec, } from "./models.js";
|
|
9
|
+
import { loadOpencodeCatalog, resolveOpencodeModels } from "./opencode.js";
|
|
9
10
|
import { readInstalledManifest, runInit } from "./init.js";
|
|
10
11
|
import { runUninstall } from "./uninstall.js";
|
|
11
12
|
function isInteractive() {
|
|
@@ -101,6 +102,7 @@ program
|
|
|
101
102
|
.option("-f, --force", "overwrite kit-owned files that have local edits")
|
|
102
103
|
.option("--harness <list>", `comma-separated harnesses (${HARNESSES.join(", ")}); default: detected`)
|
|
103
104
|
.option("--models <spec>", 'per-role model overrides, e.g. "implementer=sonnet,reviewer=opus"')
|
|
105
|
+
.option("--opencode-provider <id>", "opencode provider id for alias resolution (e.g. github-copilot); auto-detected when omitted")
|
|
104
106
|
.action(async (dir, opts) => {
|
|
105
107
|
const targetDir = requireDirectory(dir);
|
|
106
108
|
if (!targetDir)
|
|
@@ -147,11 +149,27 @@ program
|
|
|
147
149
|
models = parseModelsSpec(opts.models, models);
|
|
148
150
|
if (interactive && !opts.models)
|
|
149
151
|
models = await promptModels(models);
|
|
152
|
+
// Resolve opencode model aliases against the live catalog when the opencode
|
|
153
|
+
// harness is selected. The shell-out stays here in the CLI so runInit
|
|
154
|
+
// remains pure.
|
|
155
|
+
let opencodeModels;
|
|
156
|
+
if (harnesses.includes("opencode")) {
|
|
157
|
+
const catalog = loadOpencodeCatalog();
|
|
158
|
+
const { resolved, warnings } = resolveOpencodeModels(models, {
|
|
159
|
+
catalog,
|
|
160
|
+
explicitProvider: opts.opencodeProvider,
|
|
161
|
+
});
|
|
162
|
+
opencodeModels = resolved;
|
|
163
|
+
for (const w of warnings) {
|
|
164
|
+
process.stderr.write(`Warning: ${w}\n`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
150
167
|
const report = runInit({
|
|
151
168
|
targetDir,
|
|
152
169
|
harnesses,
|
|
153
170
|
models,
|
|
154
171
|
force: opts.force,
|
|
172
|
+
opencodeModels,
|
|
155
173
|
});
|
|
156
174
|
showPaths("Created", report.written);
|
|
157
175
|
showPaths("Updated", report.updated);
|
package/dist/init.d.ts
CHANGED
|
@@ -6,6 +6,14 @@ export interface InitOptions {
|
|
|
6
6
|
harnesses: Harness[];
|
|
7
7
|
models: Record<Role, string>;
|
|
8
8
|
force?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Resolved fully-qualified opencode model ids per role, or `undefined` to
|
|
11
|
+
* omit the `model:` frontmatter line (subagent inherits the session model).
|
|
12
|
+
* When this field is absent the fallback is `opencodeModelValue(models[role])`,
|
|
13
|
+
* which passes through a fully-qualified id and returns `undefined` for bare
|
|
14
|
+
* aliases, producing the same inherit-session-model behaviour for bare inputs.
|
|
15
|
+
*/
|
|
16
|
+
opencodeModels?: Record<Role, string | undefined>;
|
|
9
17
|
}
|
|
10
18
|
export interface Manifest {
|
|
11
19
|
kit: string;
|
package/dist/init.js
CHANGED
|
@@ -97,14 +97,18 @@ function composeClaudeAgent(role, model) {
|
|
|
97
97
|
frontmatter.push("---");
|
|
98
98
|
return [...frontmatter, "", asset.body.trimEnd(), ""].join("\n");
|
|
99
99
|
}
|
|
100
|
-
function composeOpencodeAgent(role,
|
|
100
|
+
function composeOpencodeAgent(role, modelValue) {
|
|
101
101
|
const asset = readAgentAsset(role);
|
|
102
102
|
const frontmatter = [
|
|
103
103
|
"---",
|
|
104
104
|
`description: ${yamlQuote(asset.description)}`,
|
|
105
105
|
"mode: subagent",
|
|
106
|
-
`model: ${opencodeModelValue(model)}`,
|
|
107
106
|
];
|
|
107
|
+
// Only emit `model:` when a resolved, non-empty FQ id is available.
|
|
108
|
+
// Omitting it lets the subagent inherit the session/default model.
|
|
109
|
+
if (modelValue) {
|
|
110
|
+
frontmatter.push(`model: ${modelValue}`);
|
|
111
|
+
}
|
|
108
112
|
if (READ_ONLY_ROLES.has(role)) {
|
|
109
113
|
frontmatter.push("permission:", " edit: deny");
|
|
110
114
|
}
|
|
@@ -168,8 +172,12 @@ export function runInit(options) {
|
|
|
168
172
|
installKitFile(join(".agents", "skills", SKILL_NAME, "SKILL.md"), skill);
|
|
169
173
|
}
|
|
170
174
|
if (options.harnesses.includes("opencode")) {
|
|
175
|
+
installKitFile(join(".opencode", "skills", SKILL_NAME, "SKILL.md"), skill);
|
|
171
176
|
for (const role of ROLES) {
|
|
172
|
-
|
|
177
|
+
const modelValue = options.opencodeModels !== undefined
|
|
178
|
+
? options.opencodeModels[role]
|
|
179
|
+
: opencodeModelValue(options.models[role]);
|
|
180
|
+
installKitFile(join(".opencode", "agents", `${role}.md`), composeOpencodeAgent(role, modelValue));
|
|
173
181
|
}
|
|
174
182
|
}
|
|
175
183
|
// The manifest records applied state, so it is written last and only when
|
package/dist/models.d.ts
CHANGED
|
@@ -18,7 +18,14 @@ export declare function isModelAlias(value: string): value is ModelAlias;
|
|
|
18
18
|
* full model ids, so the chosen value passes through unchanged.
|
|
19
19
|
*/
|
|
20
20
|
export declare function claudeModelValue(model: string): string;
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* opencode requires fully qualified `provider/model-id` strings. Returns the
|
|
23
|
+
* value unchanged when it already contains a provider prefix, or `undefined`
|
|
24
|
+
* for bare aliases and bare ids that cannot be resolved without a live
|
|
25
|
+
* catalog. A `undefined` return means the `model:` frontmatter line should be
|
|
26
|
+
* omitted so the subagent inherits the session model.
|
|
27
|
+
*/
|
|
28
|
+
export declare function opencodeModelValue(model: string): string | undefined;
|
|
22
29
|
/**
|
|
23
30
|
* Model values are interpolated into YAML frontmatter as plain scalars;
|
|
24
31
|
* reject anything that could break out of that position.
|
package/dist/models.js
CHANGED
|
@@ -20,16 +20,6 @@ export const DEFAULT_MODELS = {
|
|
|
20
20
|
implementer: "sonnet",
|
|
21
21
|
reviewer: "opus",
|
|
22
22
|
};
|
|
23
|
-
/**
|
|
24
|
-
* opencode expects fully qualified `provider/model-id` strings (models.dev
|
|
25
|
-
* ids). These are the current Anthropic ids for the three aliases; targets
|
|
26
|
-
* with a different provider setup can pass a custom id instead.
|
|
27
|
-
*/
|
|
28
|
-
const OPENCODE_MODEL_IDS = {
|
|
29
|
-
sonnet: "anthropic/claude-sonnet-4-6",
|
|
30
|
-
opus: "anthropic/claude-opus-4-8",
|
|
31
|
-
haiku: "anthropic/claude-haiku-4-5",
|
|
32
|
-
};
|
|
33
23
|
export function isModelAlias(value) {
|
|
34
24
|
return MODEL_ALIASES.includes(value);
|
|
35
25
|
}
|
|
@@ -40,10 +30,15 @@ export function isModelAlias(value) {
|
|
|
40
30
|
export function claudeModelValue(model) {
|
|
41
31
|
return model;
|
|
42
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* opencode requires fully qualified `provider/model-id` strings. Returns the
|
|
35
|
+
* value unchanged when it already contains a provider prefix, or `undefined`
|
|
36
|
+
* for bare aliases and bare ids that cannot be resolved without a live
|
|
37
|
+
* catalog. A `undefined` return means the `model:` frontmatter line should be
|
|
38
|
+
* omitted so the subagent inherits the session model.
|
|
39
|
+
*/
|
|
43
40
|
export function opencodeModelValue(model) {
|
|
44
|
-
|
|
45
|
-
return OPENCODE_MODEL_IDS[model];
|
|
46
|
-
return model.includes("/") ? model : `anthropic/${model}`;
|
|
41
|
+
return model.includes("/") ? model : undefined;
|
|
47
42
|
}
|
|
48
43
|
/**
|
|
49
44
|
* Model values are interpolated into YAML frontmatter as plain scalars;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ModelAlias, Role } from "./models.js";
|
|
2
|
+
/**
|
|
3
|
+
* Splits raw `opencode models` stdout into a list of trimmed non-empty lines,
|
|
4
|
+
* each of the form `provider/model-id`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseOpencodeCatalog(stdout: string): string[];
|
|
7
|
+
/**
|
|
8
|
+
* Returns the provider id: the substring before the first `/`.
|
|
9
|
+
* e.g. `github-copilot/claude-sonnet-4.6` → `github-copilot`
|
|
10
|
+
* `openrouter/anthropic/claude-sonnet-4.6` → `openrouter`
|
|
11
|
+
*/
|
|
12
|
+
export declare function providerOf(modelLine: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Infers which provider to use for alias resolution.
|
|
15
|
+
*
|
|
16
|
+
* - If `explicit` is given, it is returned as-is (no catalog check).
|
|
17
|
+
* - Otherwise the catalog is scanned for providers that offer at least one
|
|
18
|
+
* model whose id segment (the part after `provider/`) starts with `claude-`.
|
|
19
|
+
* Exactly one such provider → use it.
|
|
20
|
+
* More than one → `{ provider: undefined, ambiguous: true }`.
|
|
21
|
+
* None → `{ provider: undefined, ambiguous: false }`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function detectProvider(opts: {
|
|
24
|
+
catalog: string[];
|
|
25
|
+
explicit?: string;
|
|
26
|
+
}): {
|
|
27
|
+
provider?: string;
|
|
28
|
+
ambiguous: boolean;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Finds the best fully-qualified `provider/model-id` line for a given alias
|
|
32
|
+
* from the live catalog.
|
|
33
|
+
*
|
|
34
|
+
* Selection rules:
|
|
35
|
+
* 1. Only lines starting with `${provider}/` whose remainder starts with the
|
|
36
|
+
* family token are candidates.
|
|
37
|
+
* 2. Non-canonical variants (`-fast`, `-thinking`, `-mini`, `-latest`) are
|
|
38
|
+
* excluded unless no canonical candidates remain.
|
|
39
|
+
* 3. Among remaining candidates, the one with the highest parsed version wins.
|
|
40
|
+
*
|
|
41
|
+
* Returns `undefined` when no matching line exists.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveAlias(provider: string, alias: ModelAlias, catalog: string[]): string | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Resolves each role's model string to a fully-qualified opencode id or
|
|
46
|
+
* `undefined` (meaning: omit `model:` and inherit the session model).
|
|
47
|
+
*
|
|
48
|
+
* - Already fully-qualified values (contain `/`) pass through unchanged.
|
|
49
|
+
* - Known aliases are resolved via the live catalog + provider detection.
|
|
50
|
+
* - Unknown bare strings (not an alias, no `/`) → `undefined` + warning.
|
|
51
|
+
* - When provider detection is ambiguous or yields nothing → `undefined` +
|
|
52
|
+
* one combined warning asking the user to pass `--opencode-provider <id>`
|
|
53
|
+
* or fully-qualified `--models`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveOpencodeModels(roleModels: Record<Role, string>, opts: {
|
|
56
|
+
catalog: string[];
|
|
57
|
+
explicitProvider?: string;
|
|
58
|
+
}): {
|
|
59
|
+
resolved: Record<Role, string | undefined>;
|
|
60
|
+
warnings: string[];
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Runs `opencode models` and returns the parsed catalog. Returns an empty
|
|
64
|
+
* array on any error (binary absent, non-zero exit, timeout, etc.).
|
|
65
|
+
*/
|
|
66
|
+
export declare function loadOpencodeCatalog(): string[];
|
package/dist/opencode.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { isModelAlias } from "./models.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Pure catalog helpers (unit-testable without shell access)
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
/**
|
|
7
|
+
* Splits raw `opencode models` stdout into a list of trimmed non-empty lines,
|
|
8
|
+
* each of the form `provider/model-id`.
|
|
9
|
+
*/
|
|
10
|
+
export function parseOpencodeCatalog(stdout) {
|
|
11
|
+
return stdout
|
|
12
|
+
.split("\n")
|
|
13
|
+
.map((l) => l.trim())
|
|
14
|
+
.filter((l) => l.length > 0);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Returns the provider id: the substring before the first `/`.
|
|
18
|
+
* e.g. `github-copilot/claude-sonnet-4.6` → `github-copilot`
|
|
19
|
+
* `openrouter/anthropic/claude-sonnet-4.6` → `openrouter`
|
|
20
|
+
*/
|
|
21
|
+
export function providerOf(modelLine) {
|
|
22
|
+
return modelLine.slice(0, modelLine.indexOf("/"));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Infers which provider to use for alias resolution.
|
|
26
|
+
*
|
|
27
|
+
* - If `explicit` is given, it is returned as-is (no catalog check).
|
|
28
|
+
* - Otherwise the catalog is scanned for providers that offer at least one
|
|
29
|
+
* model whose id segment (the part after `provider/`) starts with `claude-`.
|
|
30
|
+
* Exactly one such provider → use it.
|
|
31
|
+
* More than one → `{ provider: undefined, ambiguous: true }`.
|
|
32
|
+
* None → `{ provider: undefined, ambiguous: false }`.
|
|
33
|
+
*/
|
|
34
|
+
export function detectProvider(opts) {
|
|
35
|
+
if (opts.explicit !== undefined) {
|
|
36
|
+
return { provider: opts.explicit, ambiguous: false };
|
|
37
|
+
}
|
|
38
|
+
const providers = new Set();
|
|
39
|
+
for (const line of opts.catalog) {
|
|
40
|
+
if (line.indexOf("/") <= 0)
|
|
41
|
+
continue;
|
|
42
|
+
const provider = providerOf(line);
|
|
43
|
+
const remainder = line.slice(provider.length + 1);
|
|
44
|
+
if (remainder.startsWith("claude-")) {
|
|
45
|
+
providers.add(provider);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (providers.size === 1) {
|
|
49
|
+
return { provider: [...providers][0], ambiguous: false };
|
|
50
|
+
}
|
|
51
|
+
if (providers.size > 1) {
|
|
52
|
+
return { provider: undefined, ambiguous: true };
|
|
53
|
+
}
|
|
54
|
+
return { provider: undefined, ambiguous: false };
|
|
55
|
+
}
|
|
56
|
+
// Tags that mark non-canonical variants to skip when better options exist.
|
|
57
|
+
const NON_CANONICAL_MARKERS = ["-fast", "-thinking", "-mini", "-latest"];
|
|
58
|
+
function isNonCanonical(idSegment) {
|
|
59
|
+
return NON_CANONICAL_MARKERS.some((marker) => idSegment.endsWith(marker) || idSegment.includes(marker + "-"));
|
|
60
|
+
}
|
|
61
|
+
/** Parses the numeric version from a model id after removing the family token prefix. */
|
|
62
|
+
function parseVersion(idSegment, familyToken) {
|
|
63
|
+
const suffix = idSegment.slice(familyToken.length);
|
|
64
|
+
// Split on `.` and `-`, keep only numeric parts.
|
|
65
|
+
return suffix
|
|
66
|
+
.split(/[.\-]/)
|
|
67
|
+
.filter((p) => /^\d+$/.test(p))
|
|
68
|
+
.map(Number);
|
|
69
|
+
}
|
|
70
|
+
function compareVersions(a, b) {
|
|
71
|
+
const len = Math.max(a.length, b.length);
|
|
72
|
+
for (let i = 0; i < len; i++) {
|
|
73
|
+
const av = a[i] ?? 0;
|
|
74
|
+
const bv = b[i] ?? 0;
|
|
75
|
+
if (av !== bv)
|
|
76
|
+
return av - bv;
|
|
77
|
+
}
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
const FAMILY_TOKENS = {
|
|
81
|
+
sonnet: "claude-sonnet",
|
|
82
|
+
opus: "claude-opus",
|
|
83
|
+
haiku: "claude-haiku",
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Finds the best fully-qualified `provider/model-id` line for a given alias
|
|
87
|
+
* from the live catalog.
|
|
88
|
+
*
|
|
89
|
+
* Selection rules:
|
|
90
|
+
* 1. Only lines starting with `${provider}/` whose remainder starts with the
|
|
91
|
+
* family token are candidates.
|
|
92
|
+
* 2. Non-canonical variants (`-fast`, `-thinking`, `-mini`, `-latest`) are
|
|
93
|
+
* excluded unless no canonical candidates remain.
|
|
94
|
+
* 3. Among remaining candidates, the one with the highest parsed version wins.
|
|
95
|
+
*
|
|
96
|
+
* Returns `undefined` when no matching line exists.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveAlias(provider, alias, catalog) {
|
|
99
|
+
const familyToken = FAMILY_TOKENS[alias];
|
|
100
|
+
const prefix = `${provider}/`;
|
|
101
|
+
const allCandidates = catalog.filter((line) => {
|
|
102
|
+
if (!line.startsWith(prefix))
|
|
103
|
+
return false;
|
|
104
|
+
const remainder = line.slice(prefix.length);
|
|
105
|
+
return remainder.startsWith(familyToken);
|
|
106
|
+
});
|
|
107
|
+
if (allCandidates.length === 0)
|
|
108
|
+
return undefined;
|
|
109
|
+
const canonical = allCandidates.filter((line) => !isNonCanonical(line.slice(prefix.length)));
|
|
110
|
+
const candidates = canonical.length > 0 ? canonical : allCandidates;
|
|
111
|
+
let best;
|
|
112
|
+
let bestVersion = [];
|
|
113
|
+
for (const line of candidates) {
|
|
114
|
+
const remainder = line.slice(prefix.length);
|
|
115
|
+
const version = parseVersion(remainder, familyToken);
|
|
116
|
+
if (best === undefined || compareVersions(version, bestVersion) > 0) {
|
|
117
|
+
best = line;
|
|
118
|
+
bestVersion = version;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return best;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Resolves each role's model string to a fully-qualified opencode id or
|
|
125
|
+
* `undefined` (meaning: omit `model:` and inherit the session model).
|
|
126
|
+
*
|
|
127
|
+
* - Already fully-qualified values (contain `/`) pass through unchanged.
|
|
128
|
+
* - Known aliases are resolved via the live catalog + provider detection.
|
|
129
|
+
* - Unknown bare strings (not an alias, no `/`) → `undefined` + warning.
|
|
130
|
+
* - When provider detection is ambiguous or yields nothing → `undefined` +
|
|
131
|
+
* one combined warning asking the user to pass `--opencode-provider <id>`
|
|
132
|
+
* or fully-qualified `--models`.
|
|
133
|
+
*/
|
|
134
|
+
export function resolveOpencodeModels(roleModels, opts) {
|
|
135
|
+
const { catalog, explicitProvider } = opts;
|
|
136
|
+
const resolved = {};
|
|
137
|
+
const warnings = [];
|
|
138
|
+
// Lazily detect provider once, shared across all roles.
|
|
139
|
+
let providerResult;
|
|
140
|
+
for (const [role, model] of Object.entries(roleModels)) {
|
|
141
|
+
if (model.includes("/")) {
|
|
142
|
+
// Already fully qualified — pass through.
|
|
143
|
+
resolved[role] = model;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!isModelAlias(model)) {
|
|
147
|
+
resolved[role] = undefined;
|
|
148
|
+
warnings.push(`Role "${role}": "${model}" is not a known alias and has no provider prefix; model: will be omitted (inherits session model).`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
// Known alias — need provider.
|
|
152
|
+
if (providerResult === undefined) {
|
|
153
|
+
providerResult = detectProvider({
|
|
154
|
+
catalog,
|
|
155
|
+
explicit: explicitProvider,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (providerResult.provider === undefined) {
|
|
159
|
+
// Warning will be added once after the loop.
|
|
160
|
+
resolved[role] = undefined;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const fq = resolveAlias(providerResult.provider, model, catalog);
|
|
164
|
+
if (fq === undefined) {
|
|
165
|
+
resolved[role] = undefined;
|
|
166
|
+
warnings.push(`Role "${role}": provider "${providerResult.provider}" has no "${model}" model in the catalog; model: will be omitted.`);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
resolved[role] = fq;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Emit one combined warning for ambiguous / no-provider cases.
|
|
173
|
+
if (providerResult !== undefined && providerResult.provider === undefined) {
|
|
174
|
+
const msg = providerResult.ambiguous
|
|
175
|
+
? `Multiple providers offer Claude models in the live catalog; cannot auto-detect. Pass --opencode-provider <id> or use fully-qualified --models (e.g. provider/model-id) per role to resolve aliases.`
|
|
176
|
+
: `No provider offering Claude models found in the live catalog. Pass --opencode-provider <id> or use fully-qualified --models (e.g. provider/model-id) per role to resolve aliases.`;
|
|
177
|
+
warnings.push(msg);
|
|
178
|
+
}
|
|
179
|
+
return { resolved, warnings };
|
|
180
|
+
}
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Impure shell-out (kept tiny and free of logic so tests avoid it)
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
/**
|
|
185
|
+
* Runs `opencode models` and returns the parsed catalog. Returns an empty
|
|
186
|
+
* array on any error (binary absent, non-zero exit, timeout, etc.).
|
|
187
|
+
*/
|
|
188
|
+
export function loadOpencodeCatalog() {
|
|
189
|
+
try {
|
|
190
|
+
const stdout = execFileSync("opencode", ["models"], {
|
|
191
|
+
encoding: "utf8",
|
|
192
|
+
timeout: 10_000,
|
|
193
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
194
|
+
});
|
|
195
|
+
return parseOpencodeCatalog(stdout);
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
}
|
package/dist/uninstall.js
CHANGED
|
@@ -81,6 +81,8 @@ const PRUNE_CANDIDATES = [
|
|
|
81
81
|
join(".agents", "skills", "orchestrator-workflow"),
|
|
82
82
|
join(".agents", "skills"),
|
|
83
83
|
".agents",
|
|
84
|
+
join(".opencode", "skills", "orchestrator-workflow"),
|
|
85
|
+
join(".opencode", "skills"),
|
|
84
86
|
join(".opencode", "agents"),
|
|
85
87
|
".opencode",
|
|
86
88
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orchestrator-workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Installer for an orchestrator-led agent workflow: .ai/ run state, an AGENTS.md policy section, and per-harness subagent definitions for Claude Code, OpenAI Codex, and opencode",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@types/inquirer": "^9.0.7",
|
|
52
52
|
"@types/node": "^20.11.0",
|
|
53
53
|
"prettier": "^3.8.1",
|
|
54
|
-
"tsx": "^4.
|
|
54
|
+
"tsx": "^4.22.4",
|
|
55
55
|
"typescript": "^5.3.3",
|
|
56
56
|
"vitest": "^4.1.6"
|
|
57
57
|
}
|