orchestrator-workflow 0.18.0 → 0.20.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 CHANGED
@@ -5,6 +5,97 @@ 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.20.0] - 2026-08-20
9
+
10
+ ### Added
11
+
12
+ - Tier-selection policy for the orchestrator, following up on 0.19.0's
13
+ `--tiers` rendering mechanics with the guidance that was missing: when tier
14
+ variants are installed (manifest `tiers: true`), the orchestrator picks
15
+ the effort tier per task by complexity and risk, at its own judgment. The
16
+ guidance is discretionary by design, not a rigid assignment table: the
17
+ unsuffixed default subagent is the normal case, a `-low` variant fits
18
+ mechanical, narrowly scoped tasks, `-high`/`-xhigh` fit high-risk changes,
19
+ hard problems, or repeated failed attempts, and tier choice is a conscious
20
+ decision rather than a ritual, defaulting to the unsuffixed subagent when
21
+ unsure. Ships in the generated `AGENTS.md` section's Scaling delegation
22
+ bullet list and in both of `SKILL.md`'s "Delegate implementation" and
23
+ "Delegate review" steps, each also instructing the orchestrator to record a
24
+ non-default tier choice with a one-line reason in `03-decisions.md` when
25
+ the task is non-trivial. `test/docs-consistency.test.ts` gains a new
26
+ `describe` block pinning the policy prose in `agents-md-section.md`, the
27
+ absence of a rigid tier-assignment table there, an anti-drift check that
28
+ the tier suffixes the prose names (`-low`, `-high`, `-xhigh`) actually
29
+ exist in `models.ts`'s `ROLE_TIERS.implementer`, and the rule's presence in
30
+ both `SKILL.md` delegate steps.
31
+
32
+ ## [0.19.0] - 2026-08-19
33
+
34
+ ### Added
35
+
36
+ - `init` gains `--tiers`: renders an additional per-role subagent variant
37
+ file for each non-default effort tier, alongside the one default
38
+ (unsuffixed) agent file `--profile` already installs. Off by default, like
39
+ every optional pack in this kit; a plain re-run with no `--tiers` flag
40
+ keeps whatever the previous install had, the same override-vs-persist rule
41
+ already used for `--profile`/`--models`, and there is no interactive
42
+ prompt, tiers is opt-in via the flag only. `models.ts` adds `Tier`
43
+ (`low|medium|high|xhigh`), `ROLE_TIERS` (which tiers each role gets:
44
+ explorer/task-slicer `low,medium,high`; implementer all four;
45
+ reviewer `medium,high,xhigh`), `DEFAULT_TIER` (the tier each role's plain
46
+ file already corresponds to: `medium` for explorer/task-slicer/implementer,
47
+ `high` for reviewer, never rendered as its own variant since that would
48
+ both collide with and duplicate the default file), `TIER_DEFS` (tier ->
49
+ model class + requested effort), and `CLASS_MODELS` (model class -> model
50
+ alias: `small`->`haiku`, `medium`->`sonnet`, `large`->`opus`). The default
51
+ file for every role stays byte-identical to a tiers-off install (still
52
+ `manifest.models[role]`, no `effort:` key) whether or not `--tiers` is
53
+ passed, so the reviewer's `opus` default can never be silently downgraded
54
+ by this feature; a dedicated regression test pins that. Variant files are
55
+ named `<role>-<tier>.md`; with `--profile full` and tiers on, that is 4
56
+ default files plus 9 variants (13 total). Claude Code variants carry
57
+ `model: <class alias>` and `effort: <tier>` frontmatter (plus
58
+ `disallowedTools: Edit, Write, NotebookEdit` for the read-only roles, same
59
+ as the default file). opencode variants key off the resolved model's
60
+ family, not its provider prefix: a Claude-family id (any provider fronting
61
+ a `claude-`-named model, e.g. `anthropic/claude-...`,
62
+ `github-copilot/claude-...`, or the nested
63
+ `openrouter/anthropic/claude-...`) gets `variant: high`/`variant: max` for
64
+ the `high`/`xhigh` tiers only (`low`/`medium` collapse to no effort field,
65
+ a documented opencode `variant:` limitation, not a bug), Ollama or a provider-less
66
+ id gets no effort field, and every other non-Claude-family model gets a plain
67
+ `reasoningEffort: <tier>` line; the variant's `model:` line resolves
68
+ through the same live `opencode models` catalog lookup as the base
69
+ per-role model, keyed by the tier's model class instead of by role. A tier
70
+ whose class model cannot be resolved at all renders no variant file for
71
+ that class, not a file with the `model:` line simply omitted, and the CLI
72
+ warns once per unresolved class on stderr; this guard and its warning
73
+ are opencode-scoped only, since Claude Code variants resolve `model:` from
74
+ a plain alias and need no live catalog lookup. The chosen value is
75
+ recorded in a new `tiers` boolean on
76
+ `.ai/workflow/manifest.json`; a manifest written before tiers existed (no
77
+ `tiers` key) degrades to `false`, the same per-field-degradation style
78
+ already used for a missing `profile` field. Variant files flow through the
79
+ existing `installKitFile` hash ledger, so idempotence, conflict detection,
80
+ and `uninstall` all cover them automatically with no dedicated code.
81
+ Motivated by a harness capability probe (2026-08-19): Claude Code's
82
+ `effort:` subagent frontmatter is wire-verified to reach the model request
83
+ as `output_config.effort`, which is what makes rendering per-tier
84
+ frontmatter variants worth doing at all, but the same probe also found
85
+ that the `CLAUDE_CODE_EFFORT_LEVEL` environment variable always overrides
86
+ frontmatter `effort:` on every installed agent when set, tier variants and
87
+ default files alike; README's new "Effort tiers" section documents that
88
+ override explicitly as a warning, not a footnote. README documents the
89
+ flag, the role/tier table, the tier -> model class/effort table, and the
90
+ opencode provider behavior; `INSTALL-AGENT.md` documents `--tiers` in the
91
+ init question/example and the manifest JSON shape, and states the manual
92
+ fallback path does not render tier variants at all. A new, narrowly scoped
93
+ `docs-consistency.test.ts` check enumerates the README tier table against
94
+ `ROLE_TIERS`/`DEFAULT_TIER` directly, so a role or tier added to either
95
+ without a matching table update fails loudly. Both OKF bundle docs
96
+ touching the installer (`model-preselection.md`,
97
+ `install-fence-mechanics.md`) are re-verified and re-stamped.
98
+
8
99
  ## [0.18.0] - 2026-08-18
9
100
 
10
101
  ### 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), and which model each
26
- installed subagent role should use. Suggested defaults: profile `full`;
27
- explorer `sonnet`, task-slicer `sonnet`, implementer `sonnet`, reviewer
28
- `opus`.
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, and any conflicts left in place.
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. Nothing else in the repository is modified. Locally edited files are
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. If the command reports conflicts, show them to the operator
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, and any conflicts that were left in place.
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, and file hashes, and is rewritten
212
- whenever that state changes; do not edit it by hand.
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
 
@@ -49,6 +49,16 @@ default, not a ritual.
49
49
  orchestrator may review it itself; reserve the reviewer subagent for
50
50
  changes whose risk or size warrants an independent skeptical pass. Either
51
51
  way, review is never skipped.
52
+ - When tier variants are installed (manifest `tiers: true`), the orchestrator
53
+ picks the effort tier per task by complexity and risk, at its own
54
+ judgment. The unsuffixed default subagent is the normal case; a `-low`
55
+ variant fits mechanical, narrowly scoped tasks; `-high`/`-xhigh` fit
56
+ high-risk changes, hard problems, or repeated failed attempts. Not every
57
+ role gets every tier: `-xhigh` exists only for the implementer and the
58
+ reviewer, and the reviewer's downshift is `-medium` rather than `-low`,
59
+ since its default already sits at high. Spawn only variants that are
60
+ actually installed. Tier choice is a conscious decision, not a ritual;
61
+ when unsure, use the default.
52
62
 
53
63
  ### Review gate
54
64
 
@@ -19,7 +19,11 @@ Scale the ceremony to the task. The workflow below is the default for
19
19
  non-trivial work; a trivial change (a typo, a one-line fix) may be done
20
20
  directly by the orchestrator and reviewed by it, without slicing or spawning
21
21
  subagents. Review judgment still applies to every change; only the size of
22
- the apparatus changes.
22
+ the apparatus changes. When tier variants are installed, this same
23
+ per-task discretion applies to every subagent spawn, including Discover
24
+ and Slice tasks, not just the Delegate implementation and Delegate review
25
+ steps below that name it explicitly; those two steps are instances of the
26
+ rule, not its full scope.
23
27
 
24
28
  ## Roles
25
29
 
@@ -122,39 +126,48 @@ directory and the subagents.
122
126
  enough, testable, ordered correctly, and aligned with the goal. Fix the
123
127
  slicing before any implementation starts.
124
128
  6. **Delegate implementation.** Send each implementer subagent one narrow task
125
- contract (format below). When a task's acceptance rests on a test that
126
- must fail without the change, name the mutation probes to run in the
127
- task assignment; the implementer reports each one in the output
128
- contract's `mutation_probes` field (apply the mutant for real, observe
129
- the named test fail, restore, re-verify). Hold the implementer's report
130
- to the claim-only-what-was-measured rule too: treat any verification
131
- claim there that is not backed by a check it actually ran as unverified.
132
- Record meaningful decisions in `03-decisions.md` and consolidate
129
+ contract (format below). When tier variants are installed, pick the
130
+ implementer tier (the installed `implementer-<tier>` subagents, if any) by
131
+ the task's complexity and risk, at your own judgment, defaulting to the
132
+ unsuffixed subagent when unsure; record a non-default tier choice with a
133
+ one-line reason in `03-decisions.md` when the task is non-trivial. When a
134
+ task's acceptance rests on a test that must fail without the change, name
135
+ the mutation probes to run in the task assignment; the implementer reports
136
+ each one in the output contract's `mutation_probes` field (apply the mutant
137
+ for real, observe the named test fail, restore, re-verify). Hold the
138
+ implementer's report to the claim-only-what-was-measured rule too: treat any
139
+ verification claim there that is not backed by a check it actually ran as
140
+ unverified. Record meaningful decisions in `03-decisions.md` and consolidate
133
141
  evidence in `04-implementation-summary.md`.
134
142
  7. **Delegate review.** Send the diff to the reviewer subagent, naming in the
135
- briefing the base and head revision the diff was generated from. When the
136
- reviewer's environment cannot use version control to see the diff (for
137
- example a policy-gated repository), supply the diff as a pre-generated file
138
- in the briefing instead of expecting the reviewer to derive it, and have the
139
- reviewer report explicitly if it could only reconstruct the delta some other
140
- way, rather than silently reviewing less than the full change. The reviewer
141
- checks spec compliance, architecture consistency, edge cases, security, test
142
- adequacy (including whether new tests would fail if the change were
143
- reverted), and maintainability. Findings go to `05-review-findings.md`;
144
- transfer each finding from the reviewer output contract into the table's
145
- columns as-is, keeping the Severity and Decision headers unchanged, since
146
- those two are what the orchestrator-workflow completeness reader verifies.
147
- Replace the shipped placeholder/legend row with the transferred findings;
148
- for a genuine zero-findings review, delete that row instead of leaving it in
149
- place, since the completeness reader treats an untouched placeholder row
150
- with no finding rows as the template never having been filled in. When
151
- acceptance rests on empirical or probabilistic evidence (flake rates,
152
- benchmarks, "n runs green", performance/timing numbers), the reviewer must
153
- independently reproduce it its own runs or measurements, not a re-read of
154
- the implementer's log and record the method, sample size, and result
155
- against the implementer's claim in the reviewer output contract's
156
- `reproduction` field. This does not apply to deterministic checks (a single
157
- test run, `tsc`, lint): only claims that could vary run to run trigger it.
143
+ briefing the base and head revision the diff was generated from. When tier
144
+ variants are installed, pick the reviewer tier (the installed
145
+ `reviewer-<tier>` subagents, if any) by the task's complexity and risk, at
146
+ your own judgment, defaulting to the unsuffixed subagent when unsure; record
147
+ a non-default tier choice with a one-line reason in `03-decisions.md` when
148
+ the task is non-trivial. When the reviewer's environment cannot use version
149
+ control to see the diff (for example a policy-gated repository), supply the
150
+ diff as a pre-generated file in the briefing instead of expecting the
151
+ reviewer to derive it, and have the reviewer report explicitly if it could
152
+ only reconstruct the delta some other way, rather than silently reviewing
153
+ less than the full change. The reviewer checks spec compliance, architecture
154
+ consistency, edge cases, security, test adequacy (including whether new
155
+ tests would fail if the change were reverted), and maintainability. Findings
156
+ go to `05-review-findings.md`; transfer each finding from the reviewer
157
+ output contract into the table's columns as-is, keeping the Severity and
158
+ Decision headers unchanged, since those two are what the
159
+ orchestrator-workflow completeness reader verifies. Replace the shipped
160
+ placeholder/legend row with the transferred findings; for a genuine
161
+ zero-findings review, delete that row instead of leaving it in place, since
162
+ the completeness reader treats an untouched placeholder row with no finding
163
+ rows as the template never having been filled in. When acceptance rests on
164
+ empirical or probabilistic evidence (flake rates, benchmarks, "n runs
165
+ green", performance/timing numbers), the reviewer must independently
166
+ reproduce it — its own runs or measurements, not a re-read of the
167
+ implementer's log — and record the method, sample size, and result against
168
+ the implementer's claim in the reviewer output contract's `reproduction`
169
+ field. This does not apply to deterministic checks (a single test run,
170
+ `tsc`, lint): only claims that could vary run to run trigger it.
158
171
  8. **Decide acceptance.** Accept, request fixes, defer, or escalate to the
159
172
  operator. High or critical findings block acceptance until fixed or
160
173
  explicitly waived: critical findings require operator sign-off; high
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 harnessDirs = options.harnesses.filter((harness) => harness === "claude" || harness === "opencode");
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
- report.notes.push(`${relativePath}: now untracked after the full -> ${profile} profile downgrade; run \`orchestrator-workflow uninstall\` first next time, or remove it by hand.`);
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.18.0",
3
+ "version": "0.20.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",