opencode-harness-kit 0.2.1 → 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 (51) hide show
  1. package/CHANGELOG.md +301 -0
  2. package/GLOBAL.md +50 -13
  3. package/MIGRATION.md +140 -0
  4. package/README.md +233 -116
  5. package/bin/oc-harness.mjs +474 -96
  6. package/dist/index.js +378 -155
  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 +12 -8
  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-review/SKILL.md +8 -0
  40. package/src/config.js +94 -69
  41. package/src/effort.js +59 -22
  42. package/src/index.ts +15 -2
  43. package/src/lane-usage.js +142 -0
  44. package/src/profile.js +502 -212
  45. package/src/router.ts +148 -39
  46. package/src/telemetry.ts +98 -0
  47. package/examples/muse-spark-1.3-contributor-free.json +0 -41
  48. package/profiles/deepseek-v4-flash-free.json +0 -41
  49. package/profiles/muse-split-free.json +0 -64
  50. package/profiles/space-bunny-free.json +0 -42
  51. package/profiles/x-preview-f-free.json +0 -41
package/README.md CHANGED
@@ -36,8 +36,8 @@ The three steps `init` runs, spelled out for when you want to know what
36
36
  happened (prefix with `npx` or `./bin/oc-harness.mjs` to taste):
37
37
 
38
38
  ```bash
39
- oc-harness use space-bunny-free # compile the profile
40
- 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
41
41
  oc-harness verify # prove the install is whole
42
42
  ```
43
43
 
@@ -56,12 +56,57 @@ you get two projects that share a name and nothing else.
56
56
  This kit puts the answers in a profile:
57
57
 
58
58
  ```
59
- profiles/space-bunny-free.json ← the model
60
- 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
61
62
  ```
62
63
 
63
- Same plugin source, five shipped profiles, five different behaviours. The test
64
- suite asserts that rather than taking it on faith.
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
93
+ ```
94
+
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).
65
110
 
66
111
  ## What the compiler derives
67
112
 
@@ -69,24 +114,35 @@ Given a profile, `oc-harness use` writes an `opencode.jsonc` with every
69
114
  model-dependent number already worked out and commented:
70
115
 
71
116
  ```
72
- $ oc-harness use deepseek-v4-flash-free --print
73
-
74
- // profile deepseek-v4-flash-free
75
- // context 200,000 tokens
76
- // input cap 200,000 tokens
77
- // 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
78
126
  // retention zero-retention
79
127
  //
80
128
  // Derived numbers, do not hand-edit:
81
- // compaction.reserved 24,000 (12% of the input cap)
82
- // prune threshold 176,000 (input cap minus reserve)
83
- // role effort plan=max build=high reviewer=max locate=low verify=low
84
- // 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=...
85
133
  ```
86
134
 
87
- Against a 1M-context model, the same source produces `reserved 62,915` and
88
- `prune threshold 461,373`. Nobody edits those by hand, and nobody copies a
89
- 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.
90
146
 
91
147
  ### Levels are resolved, never emitted raw
92
148
 
@@ -94,16 +150,24 @@ A role asking for `max` on a model whose top level is `xhigh` gets `xhigh`, and
94
150
  the substitution is reported:
95
151
 
96
152
  ```
97
- $ oc-harness show muse-spark-1.3-contributor-free
98
- effort levels minimal < low < medium < high < xhigh
99
- role effort
100
- plan xhigh (requested max)
101
- adjustments
102
- ! 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)
103
164
  ```
104
165
 
105
- A model with no tunable effort at all gets **no** `variant` written, rather than
106
- 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.
107
171
 
108
172
  ## Privacy is a first-class field
109
173
 
@@ -111,61 +175,42 @@ a level it does not have.
111
175
  provider does with your prompts. `retention.yml` is hand-curated for that
112
176
  reason, and a profile cannot be installed until a human has stated its terms.
113
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
+
114
183
  ```
115
184
  $ oc-harness list
116
- ▸ space-bunny-free 1,048,576 max zero-retention
117
- ✗ muse-spark-1.3-contributor-free
118
- Provider may train on your prompts and completions (Meta contributor
119
- 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
120
188
  ```
121
189
 
122
- The excluded model is still installable with `--yes`, and the warning repeats
123
- 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.
124
194
 
125
195
  ## Shipped profiles
126
196
 
127
- | profile | context | top effort | retention |
128
- |---|---|---|---|
129
- | `space-bunny-free` (default) | 1,048,576 | `max` | zero-retention |
130
- | `muse-split-free` | 1,048,576 | `max` | **may train** |
131
- | `x-preview-f-free` | 1,000,000 | `max` | zero-retention |
132
- | `longcat-2.5-preview-free` | 1,000,000 | — | zero-retention |
133
- | `deepseek-v4-flash-free` | 200,000 | `max` | zero-retention |
134
- | `muse-spark-1.3-contributor-free` | 1,048,576 | `xhigh` | **may train** |
135
-
136
- ### Split profiles
137
-
138
- A profile normally puts one model behind every role. `roleModels` is the way to
139
- split that, for when the best free model and the highest-output model are not the
140
- same model:
141
-
142
- ```jsonc
143
- "model": "space-bunny-free", // base: every role not named below
144
- "roleModels": {
145
- "plan": { "model": "muse-spark-1.3-contributor-free", "levels": ["minimal","low","medium","high","xhigh"] },
146
- "build": { "model": "muse-spark-1.3-contributor-free", "levels": ["minimal","low","medium","high","xhigh"] },
147
- "locate": { "model": "space-bunny-free", "levels": ["low","medium","high","xhigh","max"] }
148
- }
149
- ```
150
-
151
- Three things fall out of this that are easy to get wrong, so they are enforced:
152
-
153
- - **`levels` is required when the model differs.** Effort is resolved against the
154
- levels of the model the role actually runs on. `plan` asking for `max` on a
155
- model that stops at `xhigh` lands on `xhigh`, instead of writing a `max` variant
156
- the model has no meaning for.
157
- - **Retention is the least private model in the file, not the base one.** A base
158
- of `zero-retention` with a `may-train` role is a `may-train` profile. The
159
- generated header prints that, with the model responsible, and so do
160
- `oc-harness use` and `oc-harness show`.
161
- - **Each role's output cap is its own.** A split role inherits the limits of the
162
- model it names, so a `build` on a 131,072-token model cannot spend the base's
163
- 524,288.
164
-
165
- `muse-split-free` is the shipped example, and it is deliberately not the default:
166
- three of its five roles run on Muse Spark, a Meta contributor-tier endpoint whose
167
- provider may train on your prompts. Install it for public or throwaway
168
- 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.
169
214
 
170
215
  ## Requirements
171
216
 
@@ -186,15 +231,16 @@ a `.bak` next to the file, including under `--yes`.
186
231
 
187
232
  ```
188
233
  oc-harness list profiles with retention and caveats
189
- oc-harness show <id> resolved effort, derived numbers, fallback queue
190
- oc-harness init pick a free profile, then install + verify
234
+ oc-harness show <id> resolved lanes, derived numbers, fallback chains
235
+ oc-harness init pick a free profile, then install + use + verify
191
236
  oc-harness use <id> compile a profile into ~/.config/opencode/opencode.jsonc
192
237
  oc-harness use <id> --dryrun print what would be written, write nothing
193
238
  oc-harness refresh <id> re-derive a profile against the live model list
194
- oc-harness install symlink the plugin, agents, skills and GLOBAL.md
195
- oc-harness link just the plugin and instruction file, no agents or skills
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
196
241
  oc-harness verify check the install against the profile; non-zero on drift
197
242
  oc-harness agents report agents the effort tiering does not reach
243
+ oc-harness lanes report which lanes the fleet actually used
198
244
  oc-harness doctor re-check profiles against models.dev and the live config
199
245
  oc-harness baseline measure the current install
200
246
  ```
@@ -205,46 +251,85 @@ instead of printing help and reporting success.
205
251
  ### `verify` is the command that matters
206
252
 
207
253
  A generated config is only half an install. The other half is the instruction
208
- file it names, the agent files it declares, and the plugin that enforces the
209
- guard — and every one of those can be missing while the config still validates
210
- 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.
211
257
 
212
258
  That is not hypothetical. On a real install of this kit, `opencode.jsonc`
213
- pointed at a `GLOBAL.md` that did not exist, declared three agents whose markdown
214
- files had never been copied, and had `build` — the *default* agent — hand-pinned
215
- to a `may-train` model while every generated field still matched the profile.
216
- 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.
217
264
 
218
- So `verify` exits non-zero when the config has drifted from its profile, when an
219
- instruction path does not resolve, when a tiered role has no agent file, when an
220
- 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:
221
269
 
222
270
  ```
223
271
  $ oc-harness verify
224
272
 
225
- verify profile space-bunny-free -> ~/.config/opencode/opencode.jsonc
273
+ verify profile fleet-safe -> ~/.config/opencode/opencode.jsonc
226
274
 
227
- ✓ config matches profile space-bunny-free
275
+ ✓ config matches profile fleet-safe
228
276
  ✓ instruction ~/.config/opencode/GLOBAL.md resolves
277
+ ✓ prompt build.md installed
278
+ ...
229
279
  ✓ plugin linked
280
+ ✓ 4 kit skill(s) valid
281
+ ✓ 219 other skill(s) present
230
282
 
231
283
  install is consistent with the profile
232
284
  ```
233
285
 
234
- `doctor` now reports the same config drift alongside its models.dev check, because
235
- "the profile is fine" and "the installed config matches the profile" are
236
- 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.
237
289
 
238
290
  `doctor` exists because free models get withdrawn within days. It diffs each
239
291
  profile against live metadata and reports drift:
240
292
 
241
293
  ```
242
- ✓ space-bunny-free matches catalog
243
- ! muse-spark-1.3-contributor-free drift
244
- 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
245
322
  ```
246
323
 
247
- 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.
248
333
 
249
334
  ## Measuring before tuning
250
335
 
@@ -282,7 +367,7 @@ unconditional.
282
367
  **Read pressure is 11.9 whole-file reads per search.** Every production harness
283
368
  in the study in `docs/KNOWN-LIMITS.md` searches before reading; this one mostly
284
369
  did not. That is a prompt problem, not a config problem, and the fix is a rule
285
- 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.
286
371
 
287
372
  **Truncation and re-read are the same number.** 441 reads were cut by the
288
373
  200-line cap and 423 were re-read with an `offset`. Truncation was not saving
@@ -298,8 +383,8 @@ Your numbers stay on your machine; only the script is in the repo.
298
383
  |---|---|---|
299
384
  | `guard.ts` | `tool.execute.before` | Blocks destructive commands in code. Fail-closed. |
300
385
  | `prune.ts` | `experimental.chat.messages.transform` | Replaces old tool output with a one-line stub. |
301
- | `router.ts` | `config`, `event`, `chat.message` | Moves to the next profile when a route is spent. |
302
- | `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. |
303
388
 
304
389
  `guard.ts` matches against the command a shell would actually run, not the text
305
390
  it was handed. It splits on shell separators, honours quotes so that a `;`
@@ -309,6 +394,7 @@ So `cd repo && rm -rf /`, `sudo rm -rf /` and `bash -c "rm -rf /"` are all
309
394
  caught, while `git log --grep push` and `echo push` are not. `git push` is
310
395
  not guard-blocked; it falls through to the `*` ask permission so opencode
311
396
  prompts for human approval before publishing.
397
+
312
398
  Writes are scoped to the project directory, with the OS temp directory as the
313
399
  one exception GLOBAL.md allows.
314
400
 
@@ -316,6 +402,11 @@ Pruning keeps user messages, assistant prose, diffs, and the last few turns
316
402
  verbatim, and never touches the in-flight turn. Old reasoning is dropped, since
317
403
  stale reasoning is worse than none.
318
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
+
319
410
  ## Install
320
411
 
321
412
  One command if you are new here (`init` asks which free profile to run on,
@@ -330,13 +421,13 @@ Two commands, in this order, if you already know which profile you want
330
421
  checkout — they are the same program):
331
422
 
332
423
  ```bash
333
- oc-harness use space-bunny-free # writes opencode.jsonc
424
+ oc-harness use fleet-safe # writes opencode.jsonc
334
425
  oc-harness install # writes everything that file points at
335
426
  ```
336
427
 
337
- `install` symlinks (not copies) the plugin, `agents/`, `skills/` and `GLOBAL.md`
338
- into `~/.config/opencode/`, so this repo stays the single source of truth and an
339
- 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.
340
431
 
341
432
  Then restart opencode. Check it took:
342
433
 
@@ -350,9 +441,21 @@ the plugin records it in the project's `.space/active` at startup so pruning
350
441
  picks up the right thresholds. To pin a profile explicitly instead:
351
442
 
352
443
  ```bash
353
- export OPENCODE_HARNESS_PROFILE=space-bunny-free
444
+ export OPENCODE_HARNESS_PROFILE=fleet-safe
354
445
  ```
355
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
+
356
459
  ### On your phone
357
460
 
358
461
  **Android (Termux)** runs the whole kit locally — same wizard as desktop:
@@ -383,12 +486,12 @@ free-model profiles all live server-side; the phone only needs a keyboard.
383
486
  ### Permissions: why `edit` is `allow`
384
487
 
385
488
  The generated config sets `permission.edit: "allow"`, which looks like it
386
- contradicts the read-only roles. It does not, and the reason is an ordering rule
387
- 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.
388
491
 
389
492
  OpenCode appends the **global** permission rule *after* the agent's own, and
390
493
  **the last matching rule wins**. So a global `edit: "ask"` silently overrides
391
- 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
392
495
  prompt. `allow` is the permissive *implicit default* and emits no rule at all,
393
496
  which leaves each agent's `deny` deciding. Verified with `opencode agent list`:
394
497
 
@@ -402,28 +505,33 @@ verify edit -> deny
402
505
 
403
506
  Writes outside the project still ask, via `external_directory`.
404
507
 
405
- ## Roles
508
+ ## Lanes
406
509
 
407
- | role | mode | job | effort |
510
+ | lane | mode | job | effort |
408
511
  |---|---|---|---|
409
512
  | `build` | primary | writes and verifies code | middle |
410
513
  | `plan` | primary | turns a request into an executable plan | highest |
411
514
  | `reviewer` | subagent | two passes, never edits | highest |
412
515
  | `locate` | subagent | finds code, reports `file:line`, never designs | lowest |
413
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 |
414
522
 
415
523
  `locate` is deliberately *not* called `scout`: OpenCode ships a built-in `scout`
416
524
  subagent for dependency and external-docs research, and shipping our own file
417
525
  under that name shadowed it with `bash: deny` so it could not do the one thing
418
526
  the built-in exists for.
419
527
 
420
- 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
421
529
  exhaustion OpenCode forces a text-only response, so an agent that has stopped
422
530
  making progress reports rather than looping until the context is gone.
423
531
 
424
532
  ## The skill loop
425
533
 
426
- 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
427
535
  name from `skills/`:
428
536
 
429
537
  | skill | step | hands to |
@@ -445,8 +553,10 @@ decides it ships.
445
553
  ## Known limits
446
554
 
447
555
  Read [docs/KNOWN-LIMITS.md](docs/KNOWN-LIMITS.md) before relying on failover.
448
- The short version: no OpenCode plugin hook can change a live session's model,
449
- 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.
450
560
 
451
561
  ## Development
452
562
 
@@ -486,7 +596,14 @@ Measured results, and the limits they do not cross, are in
486
596
  [eval/RESULTS.md](eval/RESULTS.md).
487
597
 
488
598
  `docs/RESEARCH.md` records where the numbers came from and what was tried and
489
- 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.
490
607
 
491
608
  ## Contributing
492
609