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.
- package/CHANGELOG.md +301 -0
- package/GLOBAL.md +50 -13
- package/MIGRATION.md +140 -0
- package/README.md +276 -122
- package/bin/oc-harness.mjs +571 -135
- package/dist/index.js +453 -159
- package/docs/KNOWN-LIMITS.md +189 -4
- package/docs/RESEARCH.md +80 -5
- package/docs/ROUTING.md +254 -0
- package/lanes.json +136 -0
- package/lanes.schema.json +55 -0
- package/package.json +13 -11
- package/profiles/beam-free.json +51 -0
- package/profiles/big-pickle.json +45 -0
- package/profiles/deepseek-v4.1-flash-free.json +49 -0
- package/profiles/fleet-all.json +139 -0
- package/profiles/fleet-safe.json +139 -0
- package/profiles/glm-5.3-flash-free.json +49 -0
- package/profiles/index.json +120 -37
- package/profiles/longcat-2.5-preview-free.json +14 -10
- package/profiles/mimo-v2.6-flash-free.json +44 -0
- package/profiles/minimax-m3.1-flash-free.json +51 -0
- package/profiles/nemotron-3-ultra-free.json +44 -0
- package/profiles/nemotron-3.5-lightning-free.json +44 -0
- package/profiles/nemotron-3.5-super-vl-free.json +52 -0
- package/profiles/schema.json +30 -11
- package/{agents → prompts}/build.md +2 -35
- package/prompts/doc.md +42 -0
- package/{agents → prompts}/locate.md +1 -16
- package/prompts/longform.md +34 -0
- package/{agents → prompts}/plan.md +2 -25
- package/{agents → prompts}/reviewer.md +2 -22
- package/prompts/schema.md +30 -0
- package/prompts/sweep.md +29 -0
- package/{agents → prompts}/verify.md +6 -29
- package/prompts/vision.md +33 -0
- package/retention.yml +75 -57
- package/skills/space-build/SKILL.md +16 -0
- package/skills/space-finish/SKILL.md +0 -10
- package/skills/space-review/SKILL.md +8 -0
- package/src/config.js +94 -69
- package/src/driver/run.mjs +237 -0
- package/src/effort.js +59 -22
- package/src/index.ts +15 -2
- package/src/lane-usage.js +142 -0
- package/src/profile.js +615 -221
- package/src/router.ts +148 -39
- package/src/tasks.js +248 -0
- package/src/telemetry.ts +98 -0
- package/docs/demo/demo.gif +0 -0
- package/docs/demo/demo.mp4 +0 -0
- package/docs/demo/render.py +0 -146
- package/examples/muse-spark-1.3-contributor-free.json +0 -41
- package/profiles/deepseek-v4-flash-free.json +0 -41
- package/profiles/muse-split-free.json +0 -64
- package/profiles/space-bunny-free.json +0 -42
- package/profiles/x-preview-f-free.json +0 -41
package/README.md
CHANGED
|
@@ -3,13 +3,26 @@
|
|
|
3
3
|
[](https://github.com/pham-canh/space-harness/actions/workflows/ci.yml)
|
|
4
4
|
[](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
|
|
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
|
-

|
|
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
|
|
32
|
-
oc-harness install # link the
|
|
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/
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
|
65
|
-
|
|
66
|
-
// profile
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
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
|
|
74
|
-
// prune threshold 176,000 (input cap minus reserve)
|
|
75
|
-
// role effort plan=max build=
|
|
76
|
-
//
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
-
▸
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
115
|
-
|
|
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 |
|
|
120
|
-
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
125
|
-
| `deepseek-v4-flash-free` | 200,000 | `max` | zero-retention |
|
|
126
|
-
| `
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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
|
|
180
|
-
guard — and every one of those can be missing while the config
|
|
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
|
|
185
|
-
files had never been copied, and had `build` — the *default* agent —
|
|
186
|
-
to a `may-train` model while every generated field still matched
|
|
187
|
-
Nothing errored. `opencode agent list` simply did not show those
|
|
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
|
|
190
|
-
instruction path does not resolve, when a
|
|
191
|
-
agent
|
|
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
|
|
273
|
+
verify profile fleet-safe -> ~/.config/opencode/opencode.jsonc
|
|
197
274
|
|
|
198
|
-
✓ config matches profile
|
|
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,
|
|
206
|
-
"the profile is fine" and "the installed config matches the profile"
|
|
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
|
-
✓
|
|
214
|
-
!
|
|
215
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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, `
|
|
309
|
-
into `~/.config/opencode/`, so this repo stays the single source
|
|
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=
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
##
|
|
508
|
+
## Lanes
|
|
378
509
|
|
|
379
|
-
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|