orchestrator-workflow 0.18.0 → 0.19.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 +67 -0
- package/INSTALL-AGENT.md +41 -12
- package/README.md +100 -2
- package/dist/cli.js +51 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/init.d.ts +19 -1
- package/dist/init.js +187 -5
- package/dist/models.d.ts +29 -0
- package/dist/models.js +35 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,73 @@ All notable changes to `orchestrator-workflow` are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.19.0] - 2026-08-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `init` gains `--tiers`: renders an additional per-role subagent variant
|
|
13
|
+
file for each non-default effort tier, alongside the one default
|
|
14
|
+
(unsuffixed) agent file `--profile` already installs. Off by default, like
|
|
15
|
+
every optional pack in this kit; a plain re-run with no `--tiers` flag
|
|
16
|
+
keeps whatever the previous install had, the same override-vs-persist rule
|
|
17
|
+
already used for `--profile`/`--models`, and there is no interactive
|
|
18
|
+
prompt, tiers is opt-in via the flag only. `models.ts` adds `Tier`
|
|
19
|
+
(`low|medium|high|xhigh`), `ROLE_TIERS` (which tiers each role gets:
|
|
20
|
+
explorer/task-slicer `low,medium,high`; implementer all four;
|
|
21
|
+
reviewer `medium,high,xhigh`), `DEFAULT_TIER` (the tier each role's plain
|
|
22
|
+
file already corresponds to: `medium` for explorer/task-slicer/implementer,
|
|
23
|
+
`high` for reviewer, never rendered as its own variant since that would
|
|
24
|
+
both collide with and duplicate the default file), `TIER_DEFS` (tier ->
|
|
25
|
+
model class + requested effort), and `CLASS_MODELS` (model class -> model
|
|
26
|
+
alias: `small`->`haiku`, `medium`->`sonnet`, `large`->`opus`). The default
|
|
27
|
+
file for every role stays byte-identical to a tiers-off install (still
|
|
28
|
+
`manifest.models[role]`, no `effort:` key) whether or not `--tiers` is
|
|
29
|
+
passed, so the reviewer's `opus` default can never be silently downgraded
|
|
30
|
+
by this feature; a dedicated regression test pins that. Variant files are
|
|
31
|
+
named `<role>-<tier>.md`; with `--profile full` and tiers on, that is 4
|
|
32
|
+
default files plus 9 variants (13 total). Claude Code variants carry
|
|
33
|
+
`model: <class alias>` and `effort: <tier>` frontmatter (plus
|
|
34
|
+
`disallowedTools: Edit, Write, NotebookEdit` for the read-only roles, same
|
|
35
|
+
as the default file). opencode variants key off the resolved model's
|
|
36
|
+
family, not its provider prefix: a Claude-family id (any provider fronting
|
|
37
|
+
a `claude-`-named model, e.g. `anthropic/claude-...`,
|
|
38
|
+
`github-copilot/claude-...`, or the nested
|
|
39
|
+
`openrouter/anthropic/claude-...`) gets `variant: high`/`variant: max` for
|
|
40
|
+
the `high`/`xhigh` tiers only (`low`/`medium` collapse to no effort field,
|
|
41
|
+
a documented opencode `variant:` limitation, not a bug), Ollama or a provider-less
|
|
42
|
+
id gets no effort field, and every other non-Claude-family model gets a plain
|
|
43
|
+
`reasoningEffort: <tier>` line; the variant's `model:` line resolves
|
|
44
|
+
through the same live `opencode models` catalog lookup as the base
|
|
45
|
+
per-role model, keyed by the tier's model class instead of by role. A tier
|
|
46
|
+
whose class model cannot be resolved at all renders no variant file for
|
|
47
|
+
that class, not a file with the `model:` line simply omitted, and the CLI
|
|
48
|
+
warns once per unresolved class on stderr; this guard and its warning
|
|
49
|
+
are opencode-scoped only, since Claude Code variants resolve `model:` from
|
|
50
|
+
a plain alias and need no live catalog lookup. The chosen value is
|
|
51
|
+
recorded in a new `tiers` boolean on
|
|
52
|
+
`.ai/workflow/manifest.json`; a manifest written before tiers existed (no
|
|
53
|
+
`tiers` key) degrades to `false`, the same per-field-degradation style
|
|
54
|
+
already used for a missing `profile` field. Variant files flow through the
|
|
55
|
+
existing `installKitFile` hash ledger, so idempotence, conflict detection,
|
|
56
|
+
and `uninstall` all cover them automatically with no dedicated code.
|
|
57
|
+
Motivated by a harness capability probe (2026-08-19): Claude Code's
|
|
58
|
+
`effort:` subagent frontmatter is wire-verified to reach the model request
|
|
59
|
+
as `output_config.effort`, which is what makes rendering per-tier
|
|
60
|
+
frontmatter variants worth doing at all, but the same probe also found
|
|
61
|
+
that the `CLAUDE_CODE_EFFORT_LEVEL` environment variable always overrides
|
|
62
|
+
frontmatter `effort:` on every installed agent when set, tier variants and
|
|
63
|
+
default files alike; README's new "Effort tiers" section documents that
|
|
64
|
+
override explicitly as a warning, not a footnote. README documents the
|
|
65
|
+
flag, the role/tier table, the tier -> model class/effort table, and the
|
|
66
|
+
opencode provider behavior; `INSTALL-AGENT.md` documents `--tiers` in the
|
|
67
|
+
init question/example and the manifest JSON shape, and states the manual
|
|
68
|
+
fallback path does not render tier variants at all. A new, narrowly scoped
|
|
69
|
+
`docs-consistency.test.ts` check enumerates the README tier table against
|
|
70
|
+
`ROLE_TIERS`/`DEFAULT_TIER` directly, so a role or tier added to either
|
|
71
|
+
without a matching table update fails loudly. Both OKF bundle docs
|
|
72
|
+
touching the installer (`model-preselection.md`,
|
|
73
|
+
`install-fence-mechanics.md`) are re-verified and re-stamped.
|
|
74
|
+
|
|
8
75
|
## [0.18.0] - 2026-08-18
|
|
9
76
|
|
|
10
77
|
### Changed
|
package/INSTALL-AGENT.md
CHANGED
|
@@ -22,19 +22,22 @@ which is mutable. For a stable audit, pin the URL to a commit SHA instead
|
|
|
22
22
|
below).
|
|
23
23
|
2. **Ask you, not guess**: which harnesses should get adapters, which role
|
|
24
24
|
profile to install (`full` — every role, or `minimal` — implementer and
|
|
25
|
-
reviewer only; the reviewer is never optional),
|
|
26
|
-
installed subagent role should use
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
reviewer only; the reviewer is never optional), which model each
|
|
26
|
+
installed subagent role should use, and whether to also render effort-tier
|
|
27
|
+
subagent variants (`--tiers`; off by default, no per-tier model prompt
|
|
28
|
+
since tier models are chosen automatically). Suggested defaults: profile
|
|
29
|
+
`full`; explorer `sonnet`, task-slicer `sonnet`, implementer `sonnet`,
|
|
30
|
+
reviewer `opus`; tiers off.
|
|
29
31
|
3. **Run the non-interactive installer** with your answers:
|
|
30
|
-
`npx orchestrator-workflow init --yes --harness ... --profile ... --models
|
|
32
|
+
`npx orchestrator-workflow init --yes --harness ... --profile ... --models ... [--tiers]`.
|
|
31
33
|
If the installer reports conflicts with locally edited files, the agent
|
|
32
34
|
shows them to you and asks before any `--force` re-run.
|
|
33
35
|
4. **Manual fallback only when npx or the registry is unavailable**: create
|
|
34
36
|
the same files by hand from this repository's `assets/` directory,
|
|
35
37
|
following the byte-precise rules in step 4 below.
|
|
36
38
|
5. **Report back**: which harnesses were installed, which profile and model
|
|
37
|
-
each role uses,
|
|
39
|
+
each role uses, whether effort-tier variants were rendered, and any
|
|
40
|
+
conflicts left in place.
|
|
38
41
|
|
|
39
42
|
### Write surface
|
|
40
43
|
|
|
@@ -58,7 +61,16 @@ The per-role agent files above are the `full` profile (the default); the
|
|
|
58
61
|
`minimal` profile writes only the `implementer` and `reviewer` files for
|
|
59
62
|
Claude Code and opencode and skips `task-slicer` and `explorer` entirely.
|
|
60
63
|
Codex has no per-role files, so the profile choice does not change what it
|
|
61
|
-
gets.
|
|
64
|
+
gets. When `--tiers` is on, each installed Claude Code and opencode role
|
|
65
|
+
additionally gets one subagent file per non-default effort tier, named
|
|
66
|
+
`<role>-<tier>.md` (never a file for the role's own default tier, which
|
|
67
|
+
would collide with the plain `<role>.md` file); see the package README's
|
|
68
|
+
"Effort tiers" section for the full role/tier table and the per-harness
|
|
69
|
+
frontmatter shape. `--tiers` is off by default and has no interactive
|
|
70
|
+
prompt equivalent in the manual fallback below (nor does its negation,
|
|
71
|
+
`--no-tiers`): the automated installer is the only path that renders
|
|
72
|
+
tier-variant files; a manual scaffold (step 4) does not cover them.
|
|
73
|
+
Nothing else in the repository is modified. Locally edited files are
|
|
62
74
|
reported as conflicts and left alone, never overwritten silently; the
|
|
63
75
|
exceptions are the kit-owned surfaces: `.ai/workflow/manifest.json` (the
|
|
64
76
|
kit's state file, rewritten whenever the applied state changes) and the
|
|
@@ -90,6 +102,10 @@ steps in the repository you were asked to install into.
|
|
|
90
102
|
`sonnet`, reviewer `opus`. Accept the aliases `sonnet`, `opus`,
|
|
91
103
|
`haiku` or a full model id. Skip asking about a role's model when the
|
|
92
104
|
chosen profile does not install that role.
|
|
105
|
+
- Whether to also render effort-tier subagent variants (`--tiers`)?
|
|
106
|
+
Default: off. There is no per-tier model question: tier models are
|
|
107
|
+
chosen automatically from the tier (see the package README's "Effort
|
|
108
|
+
tiers" section for the role/tier table and the model-class mapping).
|
|
93
109
|
|
|
94
110
|
3. Run the non-interactive installer with the operator's answers:
|
|
95
111
|
|
|
@@ -97,16 +113,25 @@ steps in the repository you were asked to install into.
|
|
|
97
113
|
npx orchestrator-workflow init --yes \
|
|
98
114
|
--harness <claude,codex,opencode> \
|
|
99
115
|
--profile <minimal|full> \
|
|
100
|
-
--models "explorer=<model>,task-slicer=<model>,implementer=<model>,reviewer=<model>"
|
|
116
|
+
--models "explorer=<model>,task-slicer=<model>,implementer=<model>,reviewer=<model>" \
|
|
117
|
+
[--tiers | --no-tiers]
|
|
101
118
|
```
|
|
102
119
|
|
|
103
120
|
Omit `--profile` to keep `full` (or, on a re-run, whatever profile was
|
|
104
121
|
installed previously); omit the models for roles the chosen profile does
|
|
105
|
-
not install.
|
|
122
|
+
not install. Add `--tiers` only when the operator asked for tier
|
|
123
|
+
variants; add `--no-tiers` only when the operator explicitly wants them
|
|
124
|
+
turned off on a re-run that previously had them on; omit both to keep
|
|
125
|
+
tiers off on a fresh install, or whatever value was previously installed
|
|
126
|
+
on a re-run. If the command reports conflicts, show them to the operator
|
|
106
127
|
and ask before re-running with --force.
|
|
107
128
|
|
|
108
129
|
4. Only if npx or the registry is unavailable, scaffold manually from
|
|
109
|
-
https://github.com/LanNguyenSi/agent-dx/tree/master/packages/orchestrator-workflow/assets
|
|
130
|
+
https://github.com/LanNguyenSi/agent-dx/tree/master/packages/orchestrator-workflow/assets.
|
|
131
|
+
This manual path does not cover `--tiers`: it never renders
|
|
132
|
+
`<role>-<tier>.md` variant files, regardless of what the operator asked
|
|
133
|
+
for in step 2; tell the operator tier variants require the automated
|
|
134
|
+
installer (step 3).
|
|
110
135
|
|
|
111
136
|
- `.ai/workflow/templates/00-goal.md` through `06-handoff.md` from
|
|
112
137
|
`assets/templates/`, unchanged.
|
|
@@ -164,6 +189,7 @@ steps in the repository you were asked to install into.
|
|
|
164
189
|
"version": "0.5.0",
|
|
165
190
|
"harnesses": ["claude", "opencode"],
|
|
166
191
|
"profile": "full",
|
|
192
|
+
"tiers": false,
|
|
167
193
|
"models": {
|
|
168
194
|
"explorer": "sonnet",
|
|
169
195
|
"task-slicer": "sonnet",
|
|
@@ -177,11 +203,14 @@ steps in the repository you were asked to install into.
|
|
|
177
203
|
|
|
178
204
|
Under `minimal`, `models` only needs the `implementer` and `reviewer`
|
|
179
205
|
keys (the roles actually installed); the missing keys fall back to the
|
|
180
|
-
kit's defaults if the profile is later switched back to `full`.
|
|
206
|
+
kit's defaults if the profile is later switched back to `full`. `tiers`
|
|
207
|
+
is always `false` from this manual path, since it never renders
|
|
208
|
+
tier-variant files (see step 4's opening note above).
|
|
181
209
|
|
|
182
210
|
A manual install may leave the `files` hash map empty; a later `init`
|
|
183
211
|
run then treats existing kit files conservatively and reports conflicts
|
|
184
212
|
rather than overwriting them.
|
|
185
213
|
|
|
186
214
|
5. Report back to the operator: which harnesses were installed, which model
|
|
187
|
-
each role uses,
|
|
215
|
+
each role uses, whether effort-tier variants were rendered, and any
|
|
216
|
+
conflicts that were left in place.
|
package/README.md
CHANGED
|
@@ -195,6 +195,103 @@ Nested-path providers like `openrouter` (whose ids look like
|
|
|
195
195
|
be supplied as a fully-qualified `--models` entry, e.g.
|
|
196
196
|
`reviewer=openrouter/anthropic/claude-opus-4.8`.
|
|
197
197
|
|
|
198
|
+
## Effort tiers
|
|
199
|
+
|
|
200
|
+
`--tiers` renders an additional per-role subagent definition for each
|
|
201
|
+
non-default effort tier, alongside the one default (unsuffixed) agent file
|
|
202
|
+
`--profile` already installs. Each tier variant is a standalone subagent
|
|
203
|
+
definition, not a modification of the default file: the default file
|
|
204
|
+
(`<role>.md`) stays byte-identical to what a tiers-off install already
|
|
205
|
+
produces (still `manifest.models[role]`, no `effort:` key), and each variant
|
|
206
|
+
lives next to it as `<role>-<tier>.md`.
|
|
207
|
+
|
|
208
|
+
Default off, like every optional pack in this kit: a fresh install renders
|
|
209
|
+
no variant files unless asked. `--tiers` turns the feature on for that run,
|
|
210
|
+
`--no-tiers` turns it off; a plain re-run with neither flag keeps whatever
|
|
211
|
+
the previous install had, the same override-vs-persist rule already used
|
|
212
|
+
for `--profile` and `--models`. There is no interactive prompt for it:
|
|
213
|
+
`tiers` is opt-in/off via the flags only.
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npx orchestrator-workflow init --tiers --yes
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Turning tiers back off with `--no-tiers` after having them on follows the
|
|
220
|
+
same pattern as a `full` → `minimal` profile downgrade: `init` prints a note
|
|
221
|
+
naming the now-untracked `<role>-<tier>.md` variant files and how to remove
|
|
222
|
+
them, rather than deleting them or leaving the leftover unexplained.
|
|
223
|
+
|
|
224
|
+
**Which tiers each role gets.** A role never gets a variant file for its own
|
|
225
|
+
default tier: that would collide with, and duplicate, the default file.
|
|
226
|
+
|
|
227
|
+
| Role | Tiers available | Default tier (no variant file) |
|
|
228
|
+
|---|---|---|
|
|
229
|
+
| explorer | low, medium, high | medium |
|
|
230
|
+
| task-slicer | low, medium, high | medium |
|
|
231
|
+
| implementer | low, medium, high, xhigh | medium |
|
|
232
|
+
| reviewer | medium, high, xhigh | high |
|
|
233
|
+
|
|
234
|
+
With `--profile full` and every tier rendered, that is 4 default files plus
|
|
235
|
+
9 variant files: 13 files total per harness.
|
|
236
|
+
|
|
237
|
+
**Tier → model class → effort.** Each tier resolves to a model class and an
|
|
238
|
+
effort value:
|
|
239
|
+
|
|
240
|
+
| Tier | Model class | Model alias | Effort requested |
|
|
241
|
+
|---|---|---|---|
|
|
242
|
+
| low | small | `haiku` | `low` |
|
|
243
|
+
| medium | medium | `sonnet` | `medium` |
|
|
244
|
+
| high | medium | `sonnet` | `high` |
|
|
245
|
+
| xhigh | large | `opus` | `xhigh` |
|
|
246
|
+
|
|
247
|
+
Claude Code variants carry both a `model:` line (the class's alias) and an
|
|
248
|
+
`effort: <tier>` line in frontmatter. Read-only roles (explorer, reviewer)
|
|
249
|
+
keep `disallowedTools: Edit, Write, NotebookEdit` on their variants too.
|
|
250
|
+
|
|
251
|
+
**opencode variants key off the resolved model's family, not its provider
|
|
252
|
+
prefix**, since opencode's effort surface is not uniform across model
|
|
253
|
+
families:
|
|
254
|
+
|
|
255
|
+
- **Claude-family models** (any resolved id whose provider is
|
|
256
|
+
`anthropic/`, or whose segment after the provider prefix contains
|
|
257
|
+
`claude-`, which covers `anthropic/claude-...` as well as a Claude model
|
|
258
|
+
fronted by a different provider, e.g. `github-copilot/claude-sonnet-4.6`
|
|
259
|
+
or the nested `openrouter/anthropic/claude-opus-4.8`): only `high` and
|
|
260
|
+
`xhigh` get an effort field, as `variant: high` and `variant: max`
|
|
261
|
+
respectively; `low` and `medium` collapse to no effort field at all,
|
|
262
|
+
since opencode's `variant:` option does not distinguish an effort below
|
|
263
|
+
`high`. This collapse is deliberate and documented, not a bug: a
|
|
264
|
+
`low`/`medium` variant on a Claude-family model still gets its class's
|
|
265
|
+
`model:` line, just no `variant:` line.
|
|
266
|
+
- **Ollama, or an id with no provider prefix**: no effort field at all.
|
|
267
|
+
There is no known effort passthrough for Ollama, and an id with no `/`
|
|
268
|
+
resolves to no provider to key the decision on.
|
|
269
|
+
- **Every other non-Claude-family model**: a plain `reasoningEffort: <tier>`
|
|
270
|
+
line, `xhigh` included (opencode's built-in OpenAI-style variants
|
|
271
|
+
document an `xhigh` reasoning effort).
|
|
272
|
+
|
|
273
|
+
The variant's `model:` line is resolved the same way the base per-role model
|
|
274
|
+
is (an `opencode models` catalog lookup against the auto-detected or
|
|
275
|
+
`--opencode-provider`-specified provider), just keyed by the tier's model
|
|
276
|
+
class instead of by role. When that lookup cannot resolve a model for a
|
|
277
|
+
class, the CLI warns once on stderr and **no variant file is rendered for
|
|
278
|
+
that class at all**, not a file with a missing `model:` line: a variant
|
|
279
|
+
with no resolved model would carry neither a `model:` nor an effort line, an
|
|
280
|
+
indistinguishable no-op duplicate of the base file with no ledger entry to
|
|
281
|
+
compare it against, so `init` skips writing it entirely. This guard and its
|
|
282
|
+
warning are opencode-scoped only; Claude Code variants resolve `model:` from
|
|
283
|
+
a plain alias (`haiku`/`sonnet`/`opus`) and need no live catalog lookup, so
|
|
284
|
+
they are unaffected.
|
|
285
|
+
|
|
286
|
+
**Warning: `CLAUDE_CODE_EFFORT_LEVEL` overrides every agent's frontmatter
|
|
287
|
+
`effort:`, tier variants included.** Claude Code's `effort:` frontmatter
|
|
288
|
+
field does work: it reaches the model request as `output_config.effort`.
|
|
289
|
+
But when the harness environment sets `CLAUDE_CODE_EFFORT_LEVEL`, that
|
|
290
|
+
environment variable wins over the frontmatter `effort:` on every installed
|
|
291
|
+
agent, tier variants and default files alike, not just the one this feature
|
|
292
|
+
adds. Check for it before relying on a specific tier variant's requested
|
|
293
|
+
effort actually taking effect.
|
|
294
|
+
|
|
198
295
|
## Ownership and re-runs
|
|
199
296
|
|
|
200
297
|
`init` is idempotent: a second run changes nothing. The rules:
|
|
@@ -208,8 +305,9 @@ be supplied as a fully-qualified `--models` entry, e.g.
|
|
|
208
305
|
updates files you never touched and reports files you edited as conflicts
|
|
209
306
|
instead of overwriting them; `--force` overwrites those too.
|
|
210
307
|
- `.ai/workflow/manifest.json` is the kit's state file. It records the applied
|
|
211
|
-
version, harnesses, role profile, models,
|
|
212
|
-
whenever that state changes; do not edit it by
|
|
308
|
+
version, harnesses, role profile, models, the `--tiers` flag, and file
|
|
309
|
+
hashes, and is rewritten whenever that state changes; do not edit it by
|
|
310
|
+
hand.
|
|
213
311
|
|
|
214
312
|
## Uninstall
|
|
215
313
|
|
package/dist/cli.js
CHANGED
|
@@ -5,8 +5,8 @@ import { Command } from "commander";
|
|
|
5
5
|
import inquirer from "inquirer";
|
|
6
6
|
import { PACKAGE_VERSION } from "./assets.js";
|
|
7
7
|
import { HARNESSES, detectHarnesses, parseHarnessList } from "./detect.js";
|
|
8
|
-
import { DEFAULT_MODELS, DEFAULT_PROFILE, MODEL_ALIASES, PROFILES, assertValidModelId, parseModelsSpec, parseProfile, rolesForProfile, } from "./models.js";
|
|
9
|
-
import { loadOpencodeCatalog, resolveOpencodeModels } from "./opencode.js";
|
|
8
|
+
import { CLASS_MODELS, DEFAULT_MODELS, DEFAULT_PROFILE, MODEL_ALIASES, MODEL_CLASSES, PROFILES, assertValidModelId, parseModelsSpec, parseProfile, rolesForProfile, } from "./models.js";
|
|
9
|
+
import { detectProvider, loadOpencodeCatalog, resolveAlias, resolveOpencodeModels, } from "./opencode.js";
|
|
10
10
|
import { readInstalledManifest, runInit } from "./init.js";
|
|
11
11
|
import { runUninstall } from "./uninstall.js";
|
|
12
12
|
function isInteractive() {
|
|
@@ -125,6 +125,8 @@ program
|
|
|
125
125
|
.option("--models <spec>", 'per-role model overrides, e.g. "implementer=sonnet,reviewer=opus"')
|
|
126
126
|
.option("--profile <profile>", `subagent role profile (${PROFILES.join(", ")}); default: full, or the previously installed profile on a re-run`)
|
|
127
127
|
.option("--opencode-provider <id>", "opencode provider id for alias resolution (e.g. github-copilot); auto-detected when omitted")
|
|
128
|
+
.option("--tiers", "also render per-role effort-tier subagent variants (<role>-<tier>.md); default: off, or the previously installed value on a re-run")
|
|
129
|
+
.option("--no-tiers", "explicitly turn effort-tier subagent variants off, overriding a previously installed --tiers value")
|
|
128
130
|
.action(async (dir, opts) => {
|
|
129
131
|
const targetDir = requireDirectory(dir);
|
|
130
132
|
if (!targetDir)
|
|
@@ -148,7 +150,7 @@ program
|
|
|
148
150
|
const installedFor = previous.harnesses.length > 0
|
|
149
151
|
? previous.harnesses.join(", ")
|
|
150
152
|
: "none recorded";
|
|
151
|
-
console.log(`Found existing install (${version.startsWith("unknown") ? version : `v${version}`}, harnesses: ${installedFor}, profile: ${previous.profile})`);
|
|
153
|
+
console.log(`Found existing install (${version.startsWith("unknown") ? version : `v${version}`}, harnesses: ${installedFor}, profile: ${previous.profile}, tiers: ${previous.tiers})`);
|
|
152
154
|
}
|
|
153
155
|
let harnesses;
|
|
154
156
|
if (opts.harness) {
|
|
@@ -184,10 +186,22 @@ program
|
|
|
184
186
|
models = parseModelsSpec(opts.models, models);
|
|
185
187
|
if (interactive && !opts.models)
|
|
186
188
|
models = await promptModels(models, rolesForProfile(profile));
|
|
189
|
+
// Explicit --tiers/--no-tiers always override; a plain re-run (neither
|
|
190
|
+
// flag passed) keeps whatever the previous install had (default false
|
|
191
|
+
// for a fresh install), same override-vs-persist rule as
|
|
192
|
+
// --profile/--models above. commander's negatable-option pairing
|
|
193
|
+
// (--tiers / --no-tiers declared under the same "tiers" option name)
|
|
194
|
+
// resolves opts.tiers to `true` when --tiers is passed, `false` when
|
|
195
|
+
// --no-tiers is passed, and `undefined` when neither is passed; the
|
|
196
|
+
// CLI re-run test below verifies this against the installed commander
|
|
197
|
+
// version rather than assuming it. No interactive prompt: tiers is
|
|
198
|
+
// opt-in/off via the flags only.
|
|
199
|
+
const tiers = opts.tiers ?? previous?.tiers ?? false;
|
|
187
200
|
// Resolve opencode model aliases against the live catalog when the opencode
|
|
188
201
|
// harness is selected. The shell-out stays here in the CLI so runInit
|
|
189
202
|
// remains pure.
|
|
190
203
|
let opencodeModels;
|
|
204
|
+
let opencodeClassModels;
|
|
191
205
|
if (harnesses.includes("opencode")) {
|
|
192
206
|
const catalog = loadOpencodeCatalog();
|
|
193
207
|
const { resolved, warnings } = resolveOpencodeModels(models, {
|
|
@@ -198,6 +212,37 @@ program
|
|
|
198
212
|
for (const w of warnings) {
|
|
199
213
|
process.stderr.write(`Warning: ${w}\n`);
|
|
200
214
|
}
|
|
215
|
+
if (tiers) {
|
|
216
|
+
const providerResult = detectProvider({
|
|
217
|
+
catalog,
|
|
218
|
+
explicit: opts.opencodeProvider,
|
|
219
|
+
});
|
|
220
|
+
opencodeClassModels = {};
|
|
221
|
+
for (const modelClass of MODEL_CLASSES) {
|
|
222
|
+
const alias = CLASS_MODELS[modelClass];
|
|
223
|
+
const resolved = providerResult.provider
|
|
224
|
+
? resolveAlias(providerResult.provider, alias, catalog)
|
|
225
|
+
: undefined;
|
|
226
|
+
opencodeClassModels[modelClass] = resolved;
|
|
227
|
+
if (resolved !== undefined)
|
|
228
|
+
continue;
|
|
229
|
+
// One warning per unresolved model class: without it, every
|
|
230
|
+
// effort-tier variant keyed to this class is silently skipped
|
|
231
|
+
// (init.ts skips the variant write entirely when the class
|
|
232
|
+
// model is unresolved), with nothing on stderr saying why.
|
|
233
|
+
const reason = providerResult.provider
|
|
234
|
+
? `provider "${providerResult.provider}" has no "${alias}" model in the catalog`
|
|
235
|
+
: providerResult.ambiguous
|
|
236
|
+
? `multiple providers offer Claude models in the live catalog; cannot auto-detect`
|
|
237
|
+
: `no provider offering Claude models found in the live catalog`;
|
|
238
|
+
// States the real effect (no variant file at all, not just a
|
|
239
|
+
// missing model: line, since init.ts skips the write entirely
|
|
240
|
+
// when the class never resolves) and the real scope (opencode
|
|
241
|
+
// only: Claude Code variants resolve model: from a plain alias
|
|
242
|
+
// and need no live catalog lookup, so they are unaffected).
|
|
243
|
+
process.stderr.write(`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).\n`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
201
246
|
}
|
|
202
247
|
const report = runInit({
|
|
203
248
|
targetDir,
|
|
@@ -206,6 +251,8 @@ program
|
|
|
206
251
|
profile,
|
|
207
252
|
force: opts.force,
|
|
208
253
|
opencodeModels,
|
|
254
|
+
tiers,
|
|
255
|
+
opencodeClassModels,
|
|
209
256
|
});
|
|
210
257
|
showPaths("Created", report.written);
|
|
211
258
|
showPaths("Updated", report.updated);
|
|
@@ -213,7 +260,7 @@ program
|
|
|
213
260
|
showPaths("Conflicts (local edits kept, re-run with --force to overwrite)", report.conflicted);
|
|
214
261
|
for (const note of report.notes)
|
|
215
262
|
console.log(note);
|
|
216
|
-
console.log(`\norchestrator-workflow v${PACKAGE_VERSION} installed for: ${harnesses.join(", ")} (profile: ${profile})`);
|
|
263
|
+
console.log(`\norchestrator-workflow v${PACKAGE_VERSION} installed for: ${harnesses.join(", ")} (profile: ${profile}, tiers: ${tiers})`);
|
|
217
264
|
});
|
|
218
265
|
program
|
|
219
266
|
.command("uninstall")
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { runUninstall } from "./uninstall.js";
|
|
|
4
4
|
export type { UninstallReport } from "./uninstall.js";
|
|
5
5
|
export { detectHarnesses, parseHarnessList, HARNESSES } from "./detect.js";
|
|
6
6
|
export type { Harness } from "./detect.js";
|
|
7
|
-
export { DEFAULT_MODELS, DEFAULT_PROFILE, MODEL_ALIASES, PROFILES, ROLES, claudeModelValue, isProfile, opencodeModelValue, parseModelsSpec, parseProfile, rolesForProfile, } from "./models.js";
|
|
8
|
-
export type { ModelAlias, Profile, Role } from "./models.js";
|
|
7
|
+
export { CLASS_MODELS, DEFAULT_MODELS, DEFAULT_PROFILE, DEFAULT_TIER, MODEL_ALIASES, MODEL_CLASSES, PROFILES, ROLES, ROLE_TIERS, TIER_DEFS, claudeModelValue, isProfile, opencodeModelValue, parseModelsSpec, parseProfile, rolesForProfile, } from "./models.js";
|
|
8
|
+
export type { ModelAlias, ModelClass, Profile, Role, Tier } from "./models.js";
|
|
9
9
|
export type { Report } from "./writers.js";
|
|
10
10
|
export { PACKAGE_VERSION } from "./assets.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { runInit } from "./init.js";
|
|
2
2
|
export { runUninstall } from "./uninstall.js";
|
|
3
3
|
export { detectHarnesses, parseHarnessList, HARNESSES } from "./detect.js";
|
|
4
|
-
export { DEFAULT_MODELS, DEFAULT_PROFILE, MODEL_ALIASES, PROFILES, ROLES, claudeModelValue, isProfile, opencodeModelValue, parseModelsSpec, parseProfile, rolesForProfile, } from "./models.js";
|
|
4
|
+
export { CLASS_MODELS, DEFAULT_MODELS, DEFAULT_PROFILE, DEFAULT_TIER, MODEL_ALIASES, MODEL_CLASSES, PROFILES, ROLES, ROLE_TIERS, TIER_DEFS, claudeModelValue, isProfile, opencodeModelValue, parseModelsSpec, parseProfile, rolesForProfile, } from "./models.js";
|
|
5
5
|
export { PACKAGE_VERSION } from "./assets.js";
|
package/dist/init.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Harness } from "./detect.js";
|
|
2
|
-
import type { Profile, Role } from "./models.js";
|
|
2
|
+
import type { ModelClass, Profile, Role } from "./models.js";
|
|
3
3
|
import type { Report } from "./writers.js";
|
|
4
4
|
export interface InitOptions {
|
|
5
5
|
targetDir: string;
|
|
@@ -20,6 +20,22 @@ export interface InitOptions {
|
|
|
20
20
|
* aliases, producing the same inherit-session-model behaviour for bare inputs.
|
|
21
21
|
*/
|
|
22
22
|
opencodeModels?: Record<Role, string | undefined>;
|
|
23
|
+
/**
|
|
24
|
+
* Renders additional per-role effort-tier subagent variants
|
|
25
|
+
* (`<role>-<tier>.md`) alongside the default agent file. Defaults to
|
|
26
|
+
* `false` (today's unconditional behavior: only the default file), so
|
|
27
|
+
* existing callers that do not pass this field see no change.
|
|
28
|
+
*/
|
|
29
|
+
tiers?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Resolved fully-qualified opencode model ids per tier's model class
|
|
32
|
+
* (`small`/`medium`/`large`), or `undefined` to omit the `model:` line for
|
|
33
|
+
* that variant. Only consulted when `tiers` is true and the `opencode`
|
|
34
|
+
* harness is selected; mirrors `opencodeModels` but keyed by model class
|
|
35
|
+
* instead of role, since a tier variant's model is chosen by class, not
|
|
36
|
+
* by the role's own preselected model.
|
|
37
|
+
*/
|
|
38
|
+
opencodeClassModels?: Record<ModelClass, string | undefined>;
|
|
23
39
|
}
|
|
24
40
|
export interface Manifest {
|
|
25
41
|
kit: string;
|
|
@@ -28,6 +44,8 @@ export interface Manifest {
|
|
|
28
44
|
models: Record<Role, string>;
|
|
29
45
|
/** Which subagent roles were installed: `"minimal"` or `"full"`. */
|
|
30
46
|
profile: Profile;
|
|
47
|
+
/** Whether per-role effort-tier subagent variants were rendered. */
|
|
48
|
+
tiers: boolean;
|
|
31
49
|
/**
|
|
32
50
|
* sha256 of every kit-owned file as installed. This is how a re-run tells
|
|
33
51
|
* "upstream changed, safe to update" apart from "user edited, conflict".
|
package/dist/init.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
3
3
|
import { isAbsolute, join, normalize, sep } from "node:path";
|
|
4
4
|
import { PACKAGE_VERSION, listTemplateNames, readAgentAsset, readAsset, } from "./assets.js";
|
|
5
5
|
import { HARNESSES } from "./detect.js";
|
|
6
|
-
import { DEFAULT_PROFILE, READ_ONLY_ROLES, ROLES, assertValidModelId, claudeModelValue, isProfile, opencodeModelValue, rolesForProfile, } from "./models.js";
|
|
6
|
+
import { CLASS_MODELS, DEFAULT_PROFILE, DEFAULT_TIER, READ_ONLY_ROLES, ROLES, ROLE_TIERS, TIER_DEFS, assertValidModelId, claudeModelValue, isProfile, opencodeModelValue, rolesForProfile, } from "./models.js";
|
|
7
7
|
import { emptyReport, ensureClaudeImport, installFile, upsertMarkerSection, } from "./writers.js";
|
|
8
8
|
const SKILL_NAME = "orchestrator-workflow";
|
|
9
9
|
const MANIFEST_PATH = join(".ai", "workflow", "manifest.json");
|
|
@@ -76,12 +76,18 @@ export function readInstalledManifest(targetDir) {
|
|
|
76
76
|
const profile = typeof candidate.profile === "string" && isProfile(candidate.profile)
|
|
77
77
|
? candidate.profile
|
|
78
78
|
: DEFAULT_PROFILE;
|
|
79
|
+
// A manifest written before tiers existed carries no `tiers` field; that
|
|
80
|
+
// install never rendered variant files, so it degrades to `false` here
|
|
81
|
+
// (the same per-field-degradation style as `profile` above) rather than
|
|
82
|
+
// throwing on a legacy manifest.
|
|
83
|
+
const tiers = typeof candidate.tiers === "boolean" ? candidate.tiers : false;
|
|
79
84
|
return {
|
|
80
85
|
kit: SKILL_NAME,
|
|
81
86
|
version: typeof candidate.version === "string" ? candidate.version : "",
|
|
82
87
|
harnesses,
|
|
83
88
|
models: models,
|
|
84
89
|
profile,
|
|
90
|
+
tiers,
|
|
85
91
|
files,
|
|
86
92
|
installedAt: typeof candidate.installedAt === "string" ? candidate.installedAt : "",
|
|
87
93
|
};
|
|
@@ -122,6 +128,98 @@ function composeOpencodeAgent(role, modelValue) {
|
|
|
122
128
|
frontmatter.push("---");
|
|
123
129
|
return [...frontmatter, "", asset.body.trimEnd(), ""].join("\n");
|
|
124
130
|
}
|
|
131
|
+
function tierDescriptionSuffix(asset, tier) {
|
|
132
|
+
return `${asset.description} (Effort tier: ${tier}.)`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Composes a tier-variant sibling of `composeClaudeAgent` (`<role>-<tier>.md`).
|
|
136
|
+
* The default-tier file is never rendered here (callers skip it via
|
|
137
|
+
* `DEFAULT_TIER`), so `composeClaudeAgent`'s own output stays byte-identical
|
|
138
|
+
* whether or not tiers are on.
|
|
139
|
+
*/
|
|
140
|
+
function composeClaudeAgentVariant(role, tier) {
|
|
141
|
+
const asset = readAgentAsset(role);
|
|
142
|
+
const def = TIER_DEFS[tier];
|
|
143
|
+
const frontmatter = [
|
|
144
|
+
"---",
|
|
145
|
+
`name: ${asset.name}-${tier}`,
|
|
146
|
+
`description: ${yamlQuote(tierDescriptionSuffix(asset, tier))}`,
|
|
147
|
+
`model: ${claudeModelValue(CLASS_MODELS[def.modelClass])}`,
|
|
148
|
+
`effort: ${def.effort}`,
|
|
149
|
+
];
|
|
150
|
+
if (READ_ONLY_ROLES.has(role)) {
|
|
151
|
+
frontmatter.push("disallowedTools: Edit, Write, NotebookEdit");
|
|
152
|
+
}
|
|
153
|
+
frontmatter.push("---");
|
|
154
|
+
return [...frontmatter, "", asset.body.trimEnd(), ""].join("\n");
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Whether a resolved opencode model id belongs to the Claude family,
|
|
158
|
+
* regardless of which provider is fronting it (`anthropic/claude-...`,
|
|
159
|
+
* `github-copilot/claude-...`, `openrouter/anthropic/claude-...`, ...): the
|
|
160
|
+
* segment after the provider prefix contains `claude-`, or the id starts
|
|
161
|
+
* with `anthropic/` outright. Dispatch on family rather than provider id
|
|
162
|
+
* because the `variant:` effort surface is a property of the model being
|
|
163
|
+
* served, not of which provider happens to front it.
|
|
164
|
+
*/
|
|
165
|
+
function isClaudeFamilyModel(modelValue) {
|
|
166
|
+
if (modelValue.startsWith("anthropic/"))
|
|
167
|
+
return true;
|
|
168
|
+
const slash = modelValue.indexOf("/");
|
|
169
|
+
const remainder = slash === -1 ? "" : modelValue.slice(slash + 1);
|
|
170
|
+
return remainder.includes("claude-");
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The opencode effort surface is keyed by model family, not provider id:
|
|
174
|
+
* Claude-family models' `variant:` option only distinguishes `high` and
|
|
175
|
+
* `max` (mapped from the `high`/`xhigh` tiers; `low`/`medium` collapse to no
|
|
176
|
+
* effort field), Ollama and ids without a provider prefix have no known
|
|
177
|
+
* effort passthrough, and every other model accepts a plain
|
|
178
|
+
* `reasoningEffort:` value. An unresolved (`undefined`) model gets no
|
|
179
|
+
* effort field either, since there is then no model to key the decision on.
|
|
180
|
+
*/
|
|
181
|
+
function opencodeVariantEffortLine(tier, modelValue) {
|
|
182
|
+
if (!modelValue)
|
|
183
|
+
return undefined;
|
|
184
|
+
if (isClaudeFamilyModel(modelValue)) {
|
|
185
|
+
if (tier === "high")
|
|
186
|
+
return "variant: high";
|
|
187
|
+
if (tier === "xhigh")
|
|
188
|
+
return "variant: max";
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
const slash = modelValue.indexOf("/");
|
|
192
|
+
const provider = slash === -1 ? undefined : modelValue.slice(0, slash);
|
|
193
|
+
if (provider === undefined || provider === "ollama")
|
|
194
|
+
return undefined;
|
|
195
|
+
return `reasoningEffort: ${TIER_DEFS[tier].effort}`;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Composes a tier-variant sibling of `composeOpencodeAgent`. `effortLine` is
|
|
199
|
+
* decided once, at the single call site in the opencode tier loop of
|
|
200
|
+
* `runInit` (via `opencodeVariantEffortLine`), and passed in rather than
|
|
201
|
+
* recomputed here, so there is exactly one place that decides it instead
|
|
202
|
+
* of a second, independent source of truth for the same value.
|
|
203
|
+
*/
|
|
204
|
+
function composeOpencodeAgentVariant(role, tier, modelValue, effortLine) {
|
|
205
|
+
const asset = readAgentAsset(role);
|
|
206
|
+
const frontmatter = [
|
|
207
|
+
"---",
|
|
208
|
+
`description: ${yamlQuote(tierDescriptionSuffix(asset, tier))}`,
|
|
209
|
+
"mode: subagent",
|
|
210
|
+
];
|
|
211
|
+
if (modelValue) {
|
|
212
|
+
frontmatter.push(`model: ${modelValue}`);
|
|
213
|
+
}
|
|
214
|
+
if (effortLine) {
|
|
215
|
+
frontmatter.push(effortLine);
|
|
216
|
+
}
|
|
217
|
+
if (READ_ONLY_ROLES.has(role)) {
|
|
218
|
+
frontmatter.push("permission:", " edit: deny");
|
|
219
|
+
}
|
|
220
|
+
frontmatter.push("---");
|
|
221
|
+
return [...frontmatter, "", asset.body.trimEnd(), ""].join("\n");
|
|
222
|
+
}
|
|
125
223
|
export function runInit(options) {
|
|
126
224
|
const { targetDir } = options;
|
|
127
225
|
if (!existsSync(targetDir)) {
|
|
@@ -132,9 +230,25 @@ export function runInit(options) {
|
|
|
132
230
|
}
|
|
133
231
|
const force = options.force ?? false;
|
|
134
232
|
const profile = options.profile ?? DEFAULT_PROFILE;
|
|
233
|
+
const tiers = options.tiers ?? false;
|
|
135
234
|
const report = emptyReport();
|
|
136
235
|
const previous = readInstalledManifest(targetDir);
|
|
137
236
|
const installedFiles = {};
|
|
237
|
+
// Both leftover-note loops below are ledger-driven, not enumeration-
|
|
238
|
+
// driven: they only ever push a note for a relative path that the
|
|
239
|
+
// *previous* install actually recorded in `previous.files`, and only ever
|
|
240
|
+
// iterate `previous.harnesses` (the harnesses that install actually wrote
|
|
241
|
+
// files for), never `ROLE_TIERS`/`options.harnesses` as a source of truth.
|
|
242
|
+
// Deriving the note set from ROLE_TIERS/options.harnesses instead would
|
|
243
|
+
// (a) claim a leftover for a variant file that was never written in the
|
|
244
|
+
// first place (e.g. an opencode install whose tier-class models never
|
|
245
|
+
// resolved, so the unresolved-class guard skipped every variant write),
|
|
246
|
+
// and (b) miss a real leftover whose harness was dropped from
|
|
247
|
+
// options.harnesses this run, since that harness's files are still
|
|
248
|
+
// sitting on disk and still becoming untracked either way. The ledger
|
|
249
|
+
// (`previous.files`/`previous.harnesses`) is the only source of truth for
|
|
250
|
+
// "what did the previous install actually put on disk."
|
|
251
|
+
const previousHarnessDirs = (previous?.harnesses ?? []).filter((harness) => harness === "claude" || harness === "opencode");
|
|
138
252
|
// A full -> minimal downgrade drops explorer/task-slicer from the roles
|
|
139
253
|
// installed, but (like dropping a harness from --harness) existing role
|
|
140
254
|
// files are never deleted: they simply fall out of the manifest's file
|
|
@@ -142,12 +256,46 @@ export function runInit(options) {
|
|
|
142
256
|
// left as an unexplained, untracked leftover on disk.
|
|
143
257
|
if (previous && previous.profile === "full" && profile !== previous.profile) {
|
|
144
258
|
const droppedRoles = rolesForProfile(previous.profile).filter((role) => !rolesForProfile(profile).includes(role));
|
|
145
|
-
const
|
|
146
|
-
for (const harness of harnessDirs) {
|
|
147
|
-
const harnessDir = harness === "claude" ? ".claude" : ".opencode";
|
|
259
|
+
for (const harnessDir of previousHarnessDirs.map((harness) => harness === "claude" ? ".claude" : ".opencode")) {
|
|
148
260
|
for (const role of droppedRoles) {
|
|
149
261
|
const relativePath = join(harnessDir, "agents", `${role}.md`);
|
|
150
|
-
|
|
262
|
+
if (previous.files[relativePath] !== undefined) {
|
|
263
|
+
report.notes.push(`${relativePath}: now untracked after the full -> ${profile} profile downgrade; run \`orchestrator-workflow uninstall\` first next time, or remove it by hand.`);
|
|
264
|
+
}
|
|
265
|
+
// A dropped role that also had tiers on previously left behind its
|
|
266
|
+
// own <role>-<tier>.md variant files, not just its base file; the
|
|
267
|
+
// note above only knows about <role>.md, so those variants would go
|
|
268
|
+
// unmentioned even though they are equally untracked now. ROLE_TIERS
|
|
269
|
+
// only supplies the candidate tier suffixes to probe; the ledger
|
|
270
|
+
// check above/below is what decides whether a note is actually due.
|
|
271
|
+
for (const tier of ROLE_TIERS[role]) {
|
|
272
|
+
if (tier === DEFAULT_TIER[role])
|
|
273
|
+
continue;
|
|
274
|
+
const variantPath = join(harnessDir, "agents", `${role}-${tier}.md`);
|
|
275
|
+
if (previous.files[variantPath] !== undefined) {
|
|
276
|
+
report.notes.push(`${variantPath}: now untracked after the full -> ${profile} profile downgrade; run \`orchestrator-workflow uninstall\` first next time, or remove it by hand.`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// A tiers on -> off transition leaves each still-installed role's
|
|
283
|
+
// <role>-<tier>.md variant files behind on disk, the same untracked-
|
|
284
|
+
// leftover shape as the full -> minimal profile downgrade above (a role
|
|
285
|
+
// dropped from the profile is handled by the block above instead, so
|
|
286
|
+
// there is no overlap between the two loops). Surface it the same way:
|
|
287
|
+
// a note per file instead of a silent, unexplained leftover.
|
|
288
|
+
if (previous && previous.tiers && !tiers) {
|
|
289
|
+
for (const harnessDir of previousHarnessDirs.map((harness) => harness === "claude" ? ".claude" : ".opencode")) {
|
|
290
|
+
for (const role of rolesForProfile(profile)) {
|
|
291
|
+
for (const tier of ROLE_TIERS[role]) {
|
|
292
|
+
if (tier === DEFAULT_TIER[role])
|
|
293
|
+
continue;
|
|
294
|
+
const relativePath = join(harnessDir, "agents", `${role}-${tier}.md`);
|
|
295
|
+
if (previous.files[relativePath] !== undefined) {
|
|
296
|
+
report.notes.push(`${relativePath}: now untracked after tiers were turned off; run \`orchestrator-workflow uninstall\` first next time, or remove it by hand.`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
151
299
|
}
|
|
152
300
|
}
|
|
153
301
|
}
|
|
@@ -189,6 +337,13 @@ export function runInit(options) {
|
|
|
189
337
|
installKitFile(join(".claude", "skills", SKILL_NAME, "SKILL.md"), skill);
|
|
190
338
|
for (const role of rolesForProfile(profile)) {
|
|
191
339
|
installKitFile(join(".claude", "agents", `${role}.md`), composeClaudeAgent(role, options.models[role]));
|
|
340
|
+
if (tiers) {
|
|
341
|
+
for (const tier of ROLE_TIERS[role]) {
|
|
342
|
+
if (tier === DEFAULT_TIER[role])
|
|
343
|
+
continue;
|
|
344
|
+
installKitFile(join(".claude", "agents", `${role}-${tier}.md`), composeClaudeAgentVariant(role, tier));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
192
347
|
}
|
|
193
348
|
ensureClaudeImport(report, join(targetDir, "CLAUDE.md"));
|
|
194
349
|
}
|
|
@@ -202,6 +357,31 @@ export function runInit(options) {
|
|
|
202
357
|
? options.opencodeModels[role]
|
|
203
358
|
: opencodeModelValue(options.models[role]);
|
|
204
359
|
installKitFile(join(".opencode", "agents", `${role}.md`), composeOpencodeAgent(role, modelValue));
|
|
360
|
+
if (tiers) {
|
|
361
|
+
for (const tier of ROLE_TIERS[role]) {
|
|
362
|
+
if (tier === DEFAULT_TIER[role])
|
|
363
|
+
continue;
|
|
364
|
+
const modelClass = TIER_DEFS[tier].modelClass;
|
|
365
|
+
const variantModelValue = options.opencodeClassModels?.[modelClass];
|
|
366
|
+
if (variantModelValue === undefined) {
|
|
367
|
+
// No model resolved for this class: opencodeVariantEffortLine
|
|
368
|
+
// always returns undefined too when its modelValue argument is
|
|
369
|
+
// undefined (it short-circuits on that first), so this variant
|
|
370
|
+
// would carry neither a model: nor an effort line, a silent
|
|
371
|
+
// no-op duplicate of the base file's own (possibly also
|
|
372
|
+
// unresolved) model line, with no ledger entry to compare it
|
|
373
|
+
// against. Skip it entirely rather than write that
|
|
374
|
+
// indistinguishable file. A *resolved* model with no effort line
|
|
375
|
+
// (e.g. a low/medium tier on a Claude-family model, or any
|
|
376
|
+
// Ollama model) is NOT skipped by this check: it still renders
|
|
377
|
+
// with just its model: line, see the effort-field dispatch
|
|
378
|
+
// rules in opencodeVariantEffortLine above.
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const effortLine = opencodeVariantEffortLine(tier, variantModelValue);
|
|
382
|
+
installKitFile(join(".opencode", "agents", `${role}-${tier}.md`), composeOpencodeAgentVariant(role, tier, variantModelValue, effortLine));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
205
385
|
}
|
|
206
386
|
}
|
|
207
387
|
// The manifest records applied state, so it is written last and only when
|
|
@@ -212,6 +392,7 @@ export function runInit(options) {
|
|
|
212
392
|
harnesses: [...options.harnesses].sort(),
|
|
213
393
|
models: options.models,
|
|
214
394
|
profile,
|
|
395
|
+
tiers,
|
|
215
396
|
files: installedFiles,
|
|
216
397
|
};
|
|
217
398
|
const manifestPath = join(targetDir, MANIFEST_PATH);
|
|
@@ -222,6 +403,7 @@ export function runInit(options) {
|
|
|
222
403
|
harnesses: previous.harnesses,
|
|
223
404
|
models: previous.models,
|
|
224
405
|
profile: previous.profile,
|
|
406
|
+
tiers: previous.tiers,
|
|
225
407
|
files: previous.files,
|
|
226
408
|
}) === JSON.stringify(desired)) {
|
|
227
409
|
report.skipped.push(manifestPath);
|
package/dist/models.d.ts
CHANGED
|
@@ -54,3 +54,32 @@ export declare function assertValidModelId(model: string): void;
|
|
|
54
54
|
* the given base mapping. Unknown roles and empty values are rejected.
|
|
55
55
|
*/
|
|
56
56
|
export declare function parseModelsSpec(spec: string, base: Record<Role, string>): Record<Role, string>;
|
|
57
|
+
/**
|
|
58
|
+
* Effort tiers: additional per-role subagent variants rendered alongside the
|
|
59
|
+
* default (unsuffixed) agent file when the `tiers` feature is on. Default
|
|
60
|
+
* off; see `install-fence-mechanics.md` for the default-off pack rationale.
|
|
61
|
+
*/
|
|
62
|
+
export type Tier = "low" | "medium" | "high" | "xhigh";
|
|
63
|
+
/**
|
|
64
|
+
* Which tiers each role gets a variant file for. A tier outside a role's
|
|
65
|
+
* list is never rendered for that role (e.g. explorer/task-slicer never get
|
|
66
|
+
* an `xhigh` variant, reviewer never gets a `low` variant).
|
|
67
|
+
*/
|
|
68
|
+
export declare const ROLE_TIERS: Record<Role, Tier[]>;
|
|
69
|
+
/**
|
|
70
|
+
* The tier each role's default (unsuffixed) agent file already corresponds
|
|
71
|
+
* to. No variant file is ever rendered for this tier: rendering one would
|
|
72
|
+
* both collide with the default file's name and duplicate it.
|
|
73
|
+
*/
|
|
74
|
+
export declare const DEFAULT_TIER: Record<Role, Tier>;
|
|
75
|
+
export type ModelClass = "small" | "medium" | "large";
|
|
76
|
+
export declare const MODEL_CLASSES: ModelClass[];
|
|
77
|
+
interface TierDef {
|
|
78
|
+
modelClass: ModelClass;
|
|
79
|
+
/** Effort value requested from the harness for this tier. */
|
|
80
|
+
effort: Tier;
|
|
81
|
+
}
|
|
82
|
+
export declare const TIER_DEFS: Record<Tier, TierDef>;
|
|
83
|
+
/** Which model alias backs each tier's model class. */
|
|
84
|
+
export declare const CLASS_MODELS: Record<ModelClass, ModelAlias>;
|
|
85
|
+
export {};
|
package/dist/models.js
CHANGED
|
@@ -105,3 +105,38 @@ export function parseModelsSpec(spec, base) {
|
|
|
105
105
|
}
|
|
106
106
|
return result;
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Which tiers each role gets a variant file for. A tier outside a role's
|
|
110
|
+
* list is never rendered for that role (e.g. explorer/task-slicer never get
|
|
111
|
+
* an `xhigh` variant, reviewer never gets a `low` variant).
|
|
112
|
+
*/
|
|
113
|
+
export const ROLE_TIERS = {
|
|
114
|
+
explorer: ["low", "medium", "high"],
|
|
115
|
+
"task-slicer": ["low", "medium", "high"],
|
|
116
|
+
implementer: ["low", "medium", "high", "xhigh"],
|
|
117
|
+
reviewer: ["medium", "high", "xhigh"],
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* The tier each role's default (unsuffixed) agent file already corresponds
|
|
121
|
+
* to. No variant file is ever rendered for this tier: rendering one would
|
|
122
|
+
* both collide with the default file's name and duplicate it.
|
|
123
|
+
*/
|
|
124
|
+
export const DEFAULT_TIER = {
|
|
125
|
+
explorer: "medium",
|
|
126
|
+
"task-slicer": "medium",
|
|
127
|
+
implementer: "medium",
|
|
128
|
+
reviewer: "high",
|
|
129
|
+
};
|
|
130
|
+
export const MODEL_CLASSES = ["small", "medium", "large"];
|
|
131
|
+
export const TIER_DEFS = {
|
|
132
|
+
low: { modelClass: "small", effort: "low" },
|
|
133
|
+
medium: { modelClass: "medium", effort: "medium" },
|
|
134
|
+
high: { modelClass: "medium", effort: "high" },
|
|
135
|
+
xhigh: { modelClass: "large", effort: "xhigh" },
|
|
136
|
+
};
|
|
137
|
+
/** Which model alias backs each tier's model class. */
|
|
138
|
+
export const CLASS_MODELS = {
|
|
139
|
+
small: "haiku",
|
|
140
|
+
medium: "sonnet",
|
|
141
|
+
large: "opus",
|
|
142
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orchestrator-workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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",
|