opencode-harness-kit 0.2.0 → 0.3.3

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +301 -0
  2. package/GLOBAL.md +50 -13
  3. package/MIGRATION.md +140 -0
  4. package/README.md +276 -122
  5. package/bin/oc-harness.mjs +571 -135
  6. package/dist/index.js +453 -159
  7. package/docs/KNOWN-LIMITS.md +189 -4
  8. package/docs/RESEARCH.md +80 -5
  9. package/docs/ROUTING.md +254 -0
  10. package/lanes.json +136 -0
  11. package/lanes.schema.json +55 -0
  12. package/package.json +13 -11
  13. package/profiles/beam-free.json +51 -0
  14. package/profiles/big-pickle.json +45 -0
  15. package/profiles/deepseek-v4.1-flash-free.json +49 -0
  16. package/profiles/fleet-all.json +139 -0
  17. package/profiles/fleet-safe.json +139 -0
  18. package/profiles/glm-5.3-flash-free.json +49 -0
  19. package/profiles/index.json +120 -37
  20. package/profiles/longcat-2.5-preview-free.json +14 -10
  21. package/profiles/mimo-v2.6-flash-free.json +44 -0
  22. package/profiles/minimax-m3.1-flash-free.json +51 -0
  23. package/profiles/nemotron-3-ultra-free.json +44 -0
  24. package/profiles/nemotron-3.5-lightning-free.json +44 -0
  25. package/profiles/nemotron-3.5-super-vl-free.json +52 -0
  26. package/profiles/schema.json +30 -11
  27. package/{agents → prompts}/build.md +2 -35
  28. package/prompts/doc.md +42 -0
  29. package/{agents → prompts}/locate.md +1 -16
  30. package/prompts/longform.md +34 -0
  31. package/{agents → prompts}/plan.md +2 -25
  32. package/{agents → prompts}/reviewer.md +2 -22
  33. package/prompts/schema.md +30 -0
  34. package/prompts/sweep.md +29 -0
  35. package/{agents → prompts}/verify.md +6 -29
  36. package/prompts/vision.md +33 -0
  37. package/retention.yml +75 -57
  38. package/skills/space-build/SKILL.md +16 -0
  39. package/skills/space-finish/SKILL.md +0 -10
  40. package/skills/space-review/SKILL.md +8 -0
  41. package/src/config.js +94 -69
  42. package/src/driver/run.mjs +237 -0
  43. package/src/effort.js +59 -22
  44. package/src/index.ts +15 -2
  45. package/src/lane-usage.js +142 -0
  46. package/src/profile.js +615 -221
  47. package/src/router.ts +148 -39
  48. package/src/tasks.js +248 -0
  49. package/src/telemetry.ts +98 -0
  50. package/docs/demo/demo.gif +0 -0
  51. package/docs/demo/demo.mp4 +0 -0
  52. package/docs/demo/render.py +0 -146
  53. package/examples/muse-spark-1.3-contributor-free.json +0 -41
  54. package/profiles/deepseek-v4-flash-free.json +0 -41
  55. package/profiles/muse-split-free.json +0 -64
  56. package/profiles/space-bunny-free.json +0 -42
  57. package/profiles/x-preview-f-free.json +0 -41
package/README.md CHANGED
@@ -3,13 +3,26 @@
3
3
  [![CI](https://github.com/pham-canh/space-harness/actions/workflows/ci.yml/badge.svg)](https://github.com/pham-canh/space-harness/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/opencode-harness-kit.svg)](https://www.npmjs.com/package/opencode-harness-kit)
5
5
 
6
+ <p align="center">
7
+ <video src="docs/demo/pitch.mp4" width="720" controls></video>
8
+ </p>
9
+
10
+ *Four minutes forty-seven seconds, recorded live from a real terminal and
11
+ narrated. The take script and the voiceover are in [`video/`](video/), and
12
+ rebuilding it is documented there — including the two mistakes that cost the
13
+ most time, so the next person does not repeat them.*
14
+
6
15
  A harness kit for [OpenCode](https://opencode.ai) that is **model-agnostic by
7
16
  construction**. Switching models is a data edit, not a code change.
8
17
 
9
18
  ```bash
10
- npx oc-harness init # pick a free model, then install + verify
19
+ npx -y opencode-harness-kit init # pick a free model, then install + verify
11
20
  ```
12
21
 
22
+ <p align="center">
23
+ <img src="docs/demo-loop.gif" width="760" alt="oc-harness list showing the model-agnostic profiles, then oc-harness verify reporting a consistent install">
24
+ </p>
25
+
13
26
  then restart opencode. That is the whole quickstart. No npm account needed on
14
27
  your side — `npx` only downloads the published kit. Prefer git? The same three
15
28
  steps from a checkout (see [Install](#install)):
@@ -19,17 +32,12 @@ git clone https://github.com/pham-canh/space-harness.git
19
32
  ./bin/oc-harness.mjs init
20
33
  ```
21
34
 
22
- ![demo: init and verify in one go](docs/demo/demo.gif)
23
-
24
- Regenerate it any time with `python3 docs/demo/render.py` (needs ffmpeg); the
25
- frames are drawn from real CLI output, not typed by hand.
26
-
27
35
  The three steps `init` runs, spelled out for when you want to know what
28
36
  happened (prefix with `npx` or `./bin/oc-harness.mjs` to taste):
29
37
 
30
38
  ```bash
31
- oc-harness use space-bunny-free # compile the profile
32
- oc-harness install # link the agents, skills and GLOBAL.md
39
+ oc-harness use fleet-safe # compile the profile
40
+ oc-harness install # link the plugin, prompts, skills and GLOBAL.md
33
41
  oc-harness verify # prove the install is whole
34
42
  ```
35
43
 
@@ -48,12 +56,57 @@ you get two projects that share a name and nothing else.
48
56
  This kit puts the answers in a profile:
49
57
 
50
58
  ```
51
- profiles/space-bunny-free.json ← the model
52
- src/ ← no model name anywhere in here
59
+ profiles/fleet-safe.json ← the models
60
+ lanes.json ← the jobs
61
+ src/ ← no model name anywhere in here
62
+ ```
63
+
64
+ Same plugin source, twelve shipped profiles, twelve different behaviours. The
65
+ test suite asserts that rather than taking it on faith.
66
+
67
+ ## A fleet, not a model
68
+
69
+ The default profile is a **fleet**: ten jobs, ten free models, one session.
70
+
71
+ | lane | job | model | measured |
72
+ |---|---|---|---|
73
+ | `build` | write and verify the change | MiniMax M3.1 Flash | 3.1 s |
74
+ | `plan` | turn a request into a plan | Beam | **2.6 s** |
75
+ | `reviewer` | read a change in two passes | Nemotron 3 Ultra | 4.7 s (34 s cold) |
76
+ | `locate` | find where something lives | Nemotron 3.5 Lightning | 3.7 s |
77
+ | `verify` | run the project's checks | Big Pickle | 6.5 s |
78
+ | `vision` | read an image or a screenshot | MiMo-V2.6 Flash | 19.2 s |
79
+ | `doc` | read a long document or a spec | GLM 5.3 Flash | 5.4 s |
80
+ | `schema` | produce JSON or a JSON schema | DeepSeek V4.1 Flash | 3.4 s |
81
+ | `sweep` | sweep a lot of code mechanically | Nemotron 3.5 Super VL | 4.2 s |
82
+ | `longform` | read a megabyte, or write alternatives | LongCat 2.5 Preview | 9.4 s |
83
+
84
+ Each lane is an OpenCode agent with its own model, its own context window and
85
+ its own effort level. You do not switch models: you hand the job to the lane
86
+ that has it. A delegation inside one session runs two different models — that
87
+ was measured, not assumed:
88
+
89
+ ```
90
+ agent=build modelID=minimax-m3.1-flash-free
91
+ agent=reviewer modelID=nemotron-3-ultra-free
92
+ agent=build modelID=minimax-m3.1-flash-free
53
93
  ```
54
94
 
55
- Same plugin source, four shipped profiles, four different behaviours. The test
56
- suite asserts that rather than taking it on faith.
95
+ Why ten lanes and not one model: the free tier is genuinely uneven, and the
96
+ difference is measurable. Beam answers in 2.6 seconds with a 1M window;
97
+ MiMo reads an image in 19 and MiniMax reads the same image in 8; Nemotron
98
+ Super VL is the only model on the tier that publishes a `none` effort level,
99
+ which makes "reasoning off" a job of its own. Putting each on the job it is
100
+ good at is worth more than any single "best" model.
101
+
102
+ The default is the **safe** fleet, and it is safe by construction rather than
103
+ by label: every lane resolves to a provider with a zero-retention policy, and
104
+ six of the ten lanes run on a second-choice model to stay there. The
105
+ unrestricted fleet gives every lane the model built for it.
106
+
107
+ The full table, with the measurements behind every choice and the three
108
+ capabilities the tier turns out not to have, is in
109
+ [`docs/ROUTING.md`](docs/ROUTING.md).
57
110
 
58
111
  ## What the compiler derives
59
112
 
@@ -61,24 +114,35 @@ Given a profile, `oc-harness use` writes an `opencode.jsonc` with every
61
114
  model-dependent number already worked out and commented:
62
115
 
63
116
  ```
64
- $ oc-harness use deepseek-v4-flash-free --print
65
-
66
- // profile deepseek-v4-flash-free
67
- // context 200,000 tokens
68
- // input cap 200,000 tokens
69
- // reasoning low < high < max
117
+ $ oc-harness use fleet-safe --print
118
+
119
+ // profile fleet-safe
120
+ // model opencode/minimax-m3.1-flash-free
121
+ // lanes 10 enabled, each on the first model its chain allows
122
+ // context 1,000,000 tokens (base model)
123
+ // input cap 200,000 tokens (narrowest lane; governs the shared reserve)
124
+ // output cap 512,000 tokens
125
+ // reasoning low < medium < high < xhigh < max
70
126
  // retention zero-retention
71
127
  //
72
128
  // Derived numbers, do not hand-edit:
73
- // compaction.reserved 24,000 (12% of the input cap)
74
- // prune threshold 176,000 (input cap minus reserve)
75
- // role effort plan=max build=high reviewer=max locate=low verify=low
76
- // agent steps build=40 plan=25 reviewer=15 locate=12 verify=20
129
+ // compaction.reserved 24,000 (12% of the narrowest input cap in play)
130
+ // prune threshold 176,000 (narrowest input cap minus reserve)
131
+ // role effort plan=max build=max reviewer=max locate=unset ...
132
+ // lane models build=minimax-m3.1-flash-free (out 512,000) plan=...
77
133
  ```
78
134
 
79
- Against a 1M-context model, the same source produces `reserved 62,915` and
80
- `prune threshold 461,373`. Nobody edits those by hand, and nobody copies a
81
- 200k-tuned config onto a 1M model and wonders why it overflows.
135
+ Two numbers in there are worth explaining, because both were the wrong way
136
+ round at some point:
137
+
138
+ - **The reserve is sized by the *narrowest* lane, not the widest.** A fleet
139
+ mixes a 1M model with 200k models and `compaction.reserved` is one number
140
+ for the whole config. Sizing it by the widest model reserves 125,829 tokens
141
+ and leaves every 200k lane with 74k usable — which is where the lane stops
142
+ working.
143
+ - **A lane's model is a chain, not a pin.** Free routes get withdrawn in days.
144
+ Every lane lists the models that could serve it, best first, and the first
145
+ rung the profile's privacy ceiling allows is the one that runs it.
82
146
 
83
147
  ### Levels are resolved, never emitted raw
84
148
 
@@ -86,16 +150,24 @@ A role asking for `max` on a model whose top level is `xhigh` gets `xhigh`, and
86
150
  the substitution is reported:
87
151
 
88
152
  ```
89
- $ oc-harness show muse-spark-1.3-contributor-free
90
- effort levels minimal < low < medium < high < xhigh
91
- role effort
92
- plan xhigh (requested max)
93
- adjustments
94
- ! role plan asked for max; muse-spark-1.3-contributor-free tops out at xhigh.
153
+ $ oc-harness show fleet-safe
154
+ lanes
155
+ build minimax-m3.1-flash-free out 512,000
156
+ plan deepseek-v4.1-flash-free out 32,000
157
+ locate deepseek-v4.1-flash-free out 32,000
158
+ vision minimax-m3.1-flash-free out 512,000
159
+ ...
160
+ lane effort
161
+ plan max
162
+ locate low
163
+ sweep (unset) (requested none)
95
164
  ```
96
165
 
97
- A model with no tunable effort at all gets **no** `variant` written, rather than
98
- a level it does not have.
166
+ A model with no tunable effort at all gets **no** `variant` written, rather
167
+ than a level it does not have. OpenCode does not validate the variant it is
168
+ given — `--variant max` on a model that publishes no effort levels is accepted
169
+ and silently ignored — so resolving it here is the only thing standing between
170
+ a profile and a nonsense request.
99
171
 
100
172
  ## Privacy is a first-class field
101
173
 
@@ -103,119 +175,161 @@ a level it does not have.
103
175
  provider does with your prompts. `retention.yml` is hand-curated for that
104
176
  reason, and a profile cannot be installed until a human has stated its terms.
105
177
 
178
+ Three levels, not two. `zero-retention` keeps nothing. `retained` does not
179
+ train but keeps the data for a stated window — Beam keeps prompts for 30 days
180
+ for safety reasons, which is not the same promise, so it gets its own value.
181
+ `may-train` may use them to improve the model.
182
+
106
183
  ```
107
184
  $ oc-harness list
108
- ▸ space-bunny-free 1,048,576 max zero-retention
109
- ✗ muse-spark-1.3-contributor-free
110
- Provider may train on your prompts and completions (Meta contributor
111
- tier). Top effort is xhigh, not max.
185
+ ▸ fleet-safe 200,000 max 10 zero-retention
186
+ fleet-all 160,000 max 10 may-train
187
+ beam-free 1,048,576 max 5 retained
112
188
  ```
113
189
 
114
- The excluded model is still installable with `--yes`, and the warning repeats
115
- on install. It is not hidden, it is labelled.
190
+ The fleet's privacy is a *ceiling*, not a label. `fleet-safe` declares
191
+ `maxRetention: zero-retention` and every lane resolves to a model at or below
192
+ it; `fleet-all` declares `may-train` and every lane gets the model built for
193
+ it. One file covers ten jobs either way.
116
194
 
117
195
  ## Shipped profiles
118
196
 
119
- | profile | context | top effort | retention |
120
- |---|---|---|---|
121
- | `space-bunny-free` (default) | 1,048,576 | `max` | zero-retention |
122
- | `muse-split-free` | 1,048,576 | `max` | **may train** |
123
- | `x-preview-f-free` | 1,000,000 | `max` | zero-retention |
124
- | `longcat-2.5-preview-free` | 1,000,000 | — | zero-retention |
125
- | `deepseek-v4-flash-free` | 200,000 | `max` | zero-retention |
126
- | `muse-spark-1.3-contributor-free` | 1,048,576 | `xhigh` | **may train** |
127
-
128
- ### Split profiles
129
-
130
- A profile normally puts one model behind every role. `roleModels` is the way to
131
- split that, for when the best free model and the highest-output model are not the
132
- same model:
133
-
134
- ```jsonc
135
- "model": "space-bunny-free", // base: every role not named below
136
- "roleModels": {
137
- "plan": { "model": "muse-spark-1.3-contributor-free", "levels": ["minimal","low","medium","high","xhigh"] },
138
- "build": { "model": "muse-spark-1.3-contributor-free", "levels": ["minimal","low","medium","high","xhigh"] },
139
- "locate": { "model": "space-bunny-free", "levels": ["low","medium","high","xhigh","max"] }
140
- }
141
- ```
142
-
143
- Three things fall out of this that are easy to get wrong, so they are enforced:
144
-
145
- - **`levels` is required when the model differs.** Effort is resolved against the
146
- levels of the model the role actually runs on. `plan` asking for `max` on a
147
- model that stops at `xhigh` lands on `xhigh`, instead of writing a `max` variant
148
- the model has no meaning for.
149
- - **Retention is the least private model in the file, not the base one.** A base
150
- of `zero-retention` with a `may-train` role is a `may-train` profile. The
151
- generated header prints that, with the model responsible, and so do
152
- `oc-harness use` and `oc-harness show`.
153
- - **Each role's output cap is its own.** A split role inherits the limits of the
154
- model it names, so a `build` on a 131,072-token model cannot spend the base's
155
- 524,288.
156
-
157
- `muse-split-free` is the shipped example, and it is deliberately not the default:
158
- three of its five roles run on Muse Spark, a Meta contributor-tier endpoint whose
159
- provider may train on your prompts. Install it for public or throwaway
160
- repositories.
197
+ | profile | lanes | input cap | top effort | retention |
198
+ |---|---|---|---|---|
199
+ | `fleet-safe` (default) | 10 | 200,000 | `max` | zero-retention |
200
+ | `fleet-all` | 10 | 160,000 | `max` | **may train** |
201
+ | `minimax-m3.1-flash-free` | 5 | 1,000,000 | `max` | zero-retention |
202
+ | `beam-free` | 5 | 1,048,576 | `max` | retained |
203
+ | `deepseek-v4.1-flash-free` | 5 | 200,000 | `max` | zero-retention |
204
+ | `glm-5.3-flash-free` | 5 | 200,000 | `max` | zero-retention |
205
+ | `longcat-2.5-preview-free` | 5 | 1,000,000 | — | zero-retention |
206
+ | `nemotron-3.5-super-vl-free` | 5 | 1,048,576 | `xhigh` | **may train** |
207
+ | `nemotron-3-ultra-free` | 5 | 1,000,000 | — | **may train** |
208
+ | `nemotron-3.5-lightning-free` | 5 | 262,144 | — | **may train** |
209
+ | `mimo-v2.6-flash-free` | 5 | 200,000 | — | **may train** |
210
+ | `big-pickle` | 5 | 160,000 | — | **may train** |
211
+
212
+ The five-lane profiles run everything on one model, which is the right shape
213
+ when you want one route and want to know exactly which one.
214
+
215
+ ## Requirements
216
+
217
+ | | |
218
+ |---|---|
219
+ | Node | **22 or newer** — `engines` in package.json, and the CLI uses `node:` prefixed builtins. Node 20 fails at import time, not with a friendly message. |
220
+ | npm | 10 or newer, for `npm ci` when installing from a checkout |
221
+ | OS | macOS, Linux and Windows. No platform checks in the code; the tests run on `ubuntu-latest`. |
222
+ | Shell | none. The CLI never shells out, so no `bash`, `zsh` or `fish` setup is needed. |
223
+ | Network | `list` and `doctor` read models.dev, which is the point of both. Everything else, including `install`, `use` and `verify`, works offline. |
224
+ | Disk | it writes into `~/.config/opencode/` and nothing else. |
225
+
226
+ `~/.config/opencode/` is your live agent config. `use` and `install` write there,
227
+ `verify` only reads, and `use --dryrun` writes nothing at all. Every write keeps
228
+ a `.bak` next to the file, including under `--yes`.
161
229
 
162
230
  ## Commands
163
231
 
164
232
  ```
165
233
  oc-harness list profiles with retention and caveats
234
+ oc-harness show <id> resolved lanes, derived numbers, fallback chains
235
+ oc-harness init pick a free profile, then install + use + verify
166
236
  oc-harness use <id> compile a profile into ~/.config/opencode/opencode.jsonc
167
- oc-harness show <id> resolved effort, derived numbers, fallback queue
168
237
  oc-harness use <id> --dryrun print what would be written, write nothing
169
- oc-harness install symlink the plugin, agents, skills and GLOBAL.md
238
+ oc-harness refresh <id> re-derive a profile against the live model list
239
+ oc-harness install symlink the plugin, prompts, skills and GLOBAL.md
240
+ oc-harness link just the plugin and instruction file, no prompts or skills
170
241
  oc-harness verify check the install against the profile; non-zero on drift
171
242
  oc-harness agents report agents the effort tiering does not reach
243
+ oc-harness lanes report which lanes the fleet actually used
172
244
  oc-harness doctor re-check profiles against models.dev and the live config
173
245
  oc-harness baseline measure the current install
174
246
  ```
175
247
 
248
+ `oc-harness <anything-else>` now exits non-zero with the list of real commands,
249
+ instead of printing help and reporting success.
250
+
176
251
  ### `verify` is the command that matters
177
252
 
178
253
  A generated config is only half an install. The other half is the instruction
179
- file it names, the agent files it declares, and the plugin that enforces the
180
- guard — and every one of those can be missing while the config still validates
181
- and still looks correct.
254
+ file it names, the prompt files the lanes point at, and the plugin that
255
+ enforces the guard — and every one of those can be missing while the config
256
+ still validates and still looks correct.
182
257
 
183
258
  That is not hypothetical. On a real install of this kit, `opencode.jsonc`
184
- pointed at a `GLOBAL.md` that did not exist, declared three agents whose markdown
185
- files had never been copied, and had `build` — the *default* agent — hand-pinned
186
- to a `may-train` model while every generated field still matched the profile.
187
- Nothing errored. `opencode agent list` simply did not show those agents.
259
+ pointed at a `GLOBAL.md` that did not exist, declared three agents whose
260
+ prompt files had never been copied, and had `build` — the *default* agent —
261
+ hand-pinned to a `may-train` model while every generated field still matched
262
+ the profile. Nothing errored. `opencode agent list` simply did not show those
263
+ agents.
188
264
 
189
- So `verify` exits non-zero when the config has drifted from its profile, when an
190
- instruction path does not resolve, when a tiered role has no agent file, when an
191
- agent names a model that is not active, or when a stripped schema key is present:
265
+ So `verify` exits non-zero when the config has drifted from its profile, when
266
+ an instruction path does not resolve, when a lane has no prompt file, when an
267
+ installed agent file shadows a lane, when a lane names a model that is not the
268
+ active one, or when a stripped schema key is present:
192
269
 
193
270
  ```
194
271
  $ oc-harness verify
195
272
 
196
- verify profile space-bunny-free -> ~/.config/opencode/opencode.jsonc
273
+ verify profile fleet-safe -> ~/.config/opencode/opencode.jsonc
197
274
 
198
- ✓ config matches profile space-bunny-free
275
+ ✓ config matches profile fleet-safe
199
276
  ✓ instruction ~/.config/opencode/GLOBAL.md resolves
277
+ ✓ prompt build.md installed
278
+ ...
200
279
  ✓ plugin linked
280
+ ✓ 4 kit skill(s) valid
281
+ ✓ 219 other skill(s) present
201
282
 
202
283
  install is consistent with the profile
203
284
  ```
204
285
 
205
- `doctor` now reports the same config drift alongside its models.dev check, because
206
- "the profile is fine" and "the installed config matches the profile" are
207
- different questions and only one of them was being asked.
286
+ `doctor` now reports the same config drift alongside its models.dev check,
287
+ because "the profile is fine" and "the installed config matches the profile"
288
+ are different questions and only one of them was being asked.
208
289
 
209
290
  `doctor` exists because free models get withdrawn within days. It diffs each
210
291
  profile against live metadata and reports drift:
211
292
 
212
293
  ```
213
- ✓ space-bunny-free matches catalog
214
- ! muse-spark-1.3-contributor-free drift
215
- input 524288 -> 1048576
294
+ ✓ fleet-safe matches catalog
295
+ ! beam-free drift
296
+ output 131072 -> 262144
297
+ ```
298
+
299
+ That drift was real; `doctor` found it during development. It also reports a
300
+ lane whose whole chain has left the catalogue, which is a lane with no model
301
+ at all.
302
+
303
+ ### `lanes` is how the fleet gets smaller
304
+
305
+ Ten lanes is a hypothesis. `oc-harness lanes` counts the requests:
306
+
307
+ ```
308
+ $ oc-harness lanes
309
+
310
+ lane reqs avg ms model
311
+ ----------------------------------------------------------------
312
+ plan 7 2624 beam-free
313
+ build 21 10104 minimax-m3.1-flash-free
314
+ reviewer 2 4716 nemotron-3-ultra-free
315
+ locate 4 3680 nemotron-3.5-lightning-free
316
+ verify 2 6546 big-pickle
317
+ vision 2 19167 mimo-v2.6-flash-free
318
+ doc 2 5421 glm-5.3-flash-free
319
+ schema 1 3390 deepseek-v4.1-flash-free
320
+ sweep 3 4221 nemotron-3.5-super-vl-free
321
+ longform 2 9382 longcat-2.5-preview-free
216
322
  ```
217
323
 
218
- That drift was real; `doctor` found it during development.
324
+ A lane that is never used is a description the model reads on every turn and
325
+ a free model doing nothing. Cut it from the profile's `roles` and the lane
326
+ leaves the config with it.
327
+
328
+ One request is one assistant message, which is one round trip to the provider:
329
+ a lane that makes three tool calls makes three requests. The counters come
330
+ from `.space/telemetry.jsonl`, written by the plugin, never sent anywhere, and
331
+ the file is found by walking up from the working directory so the command
332
+ works from a subdirectory too.
219
333
 
220
334
  ## Measuring before tuning
221
335
 
@@ -253,7 +367,7 @@ unconditional.
253
367
  **Read pressure is 11.9 whole-file reads per search.** Every production harness
254
368
  in the study in `docs/KNOWN-LIMITS.md` searches before reading; this one mostly
255
369
  did not. That is a prompt problem, not a config problem, and the fix is a rule
256
- in `GLOBAL.md` and in the agent files rather than a number.
370
+ in `GLOBAL.md` and in the lane prompts rather than a number.
257
371
 
258
372
  **Truncation and re-read are the same number.** 441 reads were cut by the
259
373
  200-line cap and 423 were re-read with an `offset`. Truncation was not saving
@@ -269,8 +383,8 @@ Your numbers stay on your machine; only the script is in the repo.
269
383
  |---|---|---|
270
384
  | `guard.ts` | `tool.execute.before` | Blocks destructive commands in code. Fail-closed. |
271
385
  | `prune.ts` | `experimental.chat.messages.transform` | Replaces old tool output with a one-line stub. |
272
- | `router.ts` | `config`, `event`, `chat.message` | Moves to the next profile when a route is spent. |
273
- | `telemetry.ts` | `event`, `tool.execute.after` | Local JSONL, gitignored, never a network call. |
386
+ | `router.ts` | `config`, `event`, `chat.message` | Moves a lane to the next rung of its chain when a route is spent. |
387
+ | `telemetry.ts` | `event`, `chat.message`, `tool.execute.after` | Local JSONL, gitignored, never a network call. |
274
388
 
275
389
  `guard.ts` matches against the command a shell would actually run, not the text
276
390
  it was handed. It splits on shell separators, honours quotes so that a `;`
@@ -280,6 +394,7 @@ So `cd repo && rm -rf /`, `sudo rm -rf /` and `bash -c "rm -rf /"` are all
280
394
  caught, while `git log --grep push` and `echo push` are not. `git push` is
281
395
  not guard-blocked; it falls through to the `*` ask permission so opencode
282
396
  prompts for human approval before publishing.
397
+
283
398
  Writes are scoped to the project directory, with the OS temp directory as the
284
399
  one exception GLOBAL.md allows.
285
400
 
@@ -287,6 +402,11 @@ Pruning keeps user messages, assistant prose, diffs, and the last few turns
287
402
  verbatim, and never touches the in-flight turn. Old reasoning is dropped, since
288
403
  stale reasoning is worse than none.
289
404
 
405
+ `router.ts` fails over **per lane**. A fleet has ten routes, so one spent
406
+ route must not take the other nine down with it: the lane that used the dead
407
+ model moves to the next rung of its own chain, and every other lane keeps the
408
+ model it had.
409
+
290
410
  ## Install
291
411
 
292
412
  One command if you are new here (`init` asks which free profile to run on,
@@ -301,13 +421,13 @@ Two commands, in this order, if you already know which profile you want
301
421
  checkout — they are the same program):
302
422
 
303
423
  ```bash
304
- oc-harness use space-bunny-free # writes opencode.jsonc
424
+ oc-harness use fleet-safe # writes opencode.jsonc
305
425
  oc-harness install # writes everything that file points at
306
426
  ```
307
427
 
308
- `install` symlinks (not copies) the plugin, `agents/`, `skills/` and `GLOBAL.md`
309
- into `~/.config/opencode/`, so this repo stays the single source of truth and an
310
- edit here takes effect on the next opencode start.
428
+ `install` symlinks (not copies) the plugin, `prompts/`, `skills/` and
429
+ `GLOBAL.md` into `~/.config/opencode/`, so this repo stays the single source
430
+ of truth and an edit here takes effect on the next opencode start.
311
431
 
312
432
  Then restart opencode. Check it took:
313
433
 
@@ -321,9 +441,21 @@ the plugin records it in the project's `.space/active` at startup so pruning
321
441
  picks up the right thresholds. To pin a profile explicitly instead:
322
442
 
323
443
  ```bash
324
- export OPENCODE_HARNESS_PROFILE=space-bunny-free
444
+ export OPENCODE_HARNESS_PROFILE=fleet-safe
325
445
  ```
326
446
 
447
+ ### Why the model is in the config and not in a file
448
+
449
+ An agent markdown file of the same name replaces the config entry wholesale,
450
+ and a file with no `model:` line makes the agent inherit the caller's model.
451
+ Both were observed on OpenCode 1.19, and both fail silently: the config still
452
+ looks correct and the lane runs on a model nobody chose.
453
+
454
+ So the lanes live in `agent.<lane>` in the generated config, their prompt
455
+ bodies live in `prompts/`, and `oc-harness verify` fails when an installed
456
+ agent file shadows a lane. `oc-harness install` removes the agent files an
457
+ earlier version of this kit left behind, for exactly that reason.
458
+
327
459
  ### On your phone
328
460
 
329
461
  **Android (Termux)** runs the whole kit locally — same wizard as desktop:
@@ -333,8 +465,7 @@ pkg install nodejs git
333
465
  npx -y opencode-harness-kit init
334
466
  ```
335
467
 
336
- Not published to npm yet, or no registry access? The checkout path works
337
- identically:
468
+ Without registry access, the checkout path works identically:
338
469
 
339
470
  ```bash
340
471
  pkg install nodejs git
@@ -355,12 +486,12 @@ free-model profiles all live server-side; the phone only needs a keyboard.
355
486
  ### Permissions: why `edit` is `allow`
356
487
 
357
488
  The generated config sets `permission.edit: "allow"`, which looks like it
358
- contradicts the read-only roles. It does not, and the reason is an ordering rule
359
- worth knowing before you edit these by hand.
489
+ contradicts the read-only lanes. It does not, and the reason is an ordering
490
+ rule worth knowing before you edit these by hand.
360
491
 
361
492
  OpenCode appends the **global** permission rule *after* the agent's own, and
362
493
  **the last matching rule wins**. So a global `edit: "ask"` silently overrides
363
- every agent's `edit: "deny"` — and the read-only roles can edit files after a
494
+ every agent's `edit: "deny"` — and the read-only lanes can edit files after a
364
495
  prompt. `allow` is the permissive *implicit default* and emits no rule at all,
365
496
  which leaves each agent's `deny` deciding. Verified with `opencode agent list`:
366
497
 
@@ -374,28 +505,33 @@ verify edit -> deny
374
505
 
375
506
  Writes outside the project still ask, via `external_directory`.
376
507
 
377
- ## Roles
508
+ ## Lanes
378
509
 
379
- | role | mode | job | effort |
510
+ | lane | mode | job | effort |
380
511
  |---|---|---|---|
381
512
  | `build` | primary | writes and verifies code | middle |
382
513
  | `plan` | primary | turns a request into an executable plan | highest |
383
514
  | `reviewer` | subagent | two passes, never edits | highest |
384
515
  | `locate` | subagent | finds code, reports `file:line`, never designs | lowest |
385
516
  | `verify` | subagent | runs checks, never fixes them | lowest |
517
+ | `vision` | subagent | reads an image or a screenshot | model's |
518
+ | `doc` | subagent | reads a long document or a spec | model's |
519
+ | `schema` | subagent | produces JSON a program can parse | highest |
520
+ | `sweep` | subagent | bulk mechanical sweep, no judgement | lowest |
521
+ | `longform` | subagent | very long context, or several answers | model's |
386
522
 
387
523
  `locate` is deliberately *not* called `scout`: OpenCode ships a built-in `scout`
388
524
  subagent for dependency and external-docs research, and shipping our own file
389
525
  under that name shadowed it with `bash: deny` so it could not do the one thing
390
526
  the built-in exists for.
391
527
 
392
- Each role carries a `steps` cap. It is a loop breaker, not a quality knob: on
528
+ Each lane carries a `steps` cap. It is a loop breaker, not a quality knob: on
393
529
  exhaustion OpenCode forces a text-only response, so an agent that has stopped
394
530
  making progress reports rather than looping until the context is gone.
395
531
 
396
532
  ## The skill loop
397
533
 
398
- The roles above are the *who*. The skills are the *order*, and they load by
534
+ The lanes above are the *who*. The skills are the *order*, and they load by
399
535
  name from `skills/`:
400
536
 
401
537
  | skill | step | hands to |
@@ -417,8 +553,10 @@ decides it ships.
417
553
  ## Known limits
418
554
 
419
555
  Read [docs/KNOWN-LIMITS.md](docs/KNOWN-LIMITS.md) before relying on failover.
420
- The short version: no OpenCode plugin hook can change a live session's model,
421
- so switching happens at the next session, not mid-flight.
556
+ The short version: no OpenCode plugin hook can change the model of a running
557
+ session, so switching happens at the next session, not mid-flight. What a
558
+ plugin *can* do is move a lane to the next rung of its chain before the
559
+ session starts.
422
560
 
423
561
  ## Development
424
562
 
@@ -458,7 +596,23 @@ Measured results, and the limits they do not cross, are in
458
596
  [eval/RESULTS.md](eval/RESULTS.md).
459
597
 
460
598
  `docs/RESEARCH.md` records where the numbers came from and what was tried and
461
- rejected.
599
+ rejected. `docs/ROUTING.md` records the fleet: which model serves which job,
600
+ and the measurements behind each choice.
601
+
602
+ [`CHANGELOG.md`](CHANGELOG.md) records what each release changed and, for the
603
+ releases before this one, what was broken in it — the silent failures that a
604
+ config could look correct through. [`MIGRATION.md`](MIGRATION.md) covers
605
+ 0.2.x → 0.3.0: the profile ids changed, the config layout changed, and the
606
+ telemetry file now means something different.
607
+
608
+ ## Contributing
609
+
610
+ [`CONTRIBUTING.md`](CONTRIBUTING.md) — the bar is that every change is
611
+ verifiable. Offline tests stay offline, anything that writes to disk needs a
612
+ `--dryrun` and a backup, and a number that moves updates the README in the same
613
+ commit. Bug reports go to [SECURITY.md](SECURITY.md) if they involve a key or
614
+ someone else's config; otherwise open an issue with the command you ran and what
615
+ it printed.
462
616
 
463
617
  ## License
464
618