opencode-harness-kit 0.2.1 → 0.3.4
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 +303 -0
- package/GLOBAL.md +50 -13
- package/MIGRATION.md +140 -0
- package/README.md +233 -116
- package/bin/oc-harness.mjs +474 -96
- package/dist/index.js +378 -155
- 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 +12 -8
- 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-review/SKILL.md +8 -0
- package/src/config.js +94 -69
- package/src/effort.js +59 -22
- package/src/index.ts +15 -2
- package/src/lane-usage.js +142 -0
- package/src/profile.js +502 -212
- package/src/router.ts +148 -39
- package/src/telemetry.ts +98 -0
- 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
|
@@ -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
|
|
40
|
-
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
|
|
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/
|
|
60
|
-
|
|
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,
|
|
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
|
|
73
|
-
|
|
74
|
-
// profile
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
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
|
|
82
|
-
// prune threshold 176,000 (input cap minus reserve)
|
|
83
|
-
// role effort plan=max build=
|
|
84
|
-
//
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
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
|
-
▸
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
123
|
-
|
|
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 |
|
|
128
|
-
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `deepseek-v4-flash-free` | 200,000 | `max` | zero-retention |
|
|
134
|
-
| `
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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,
|
|
195
|
-
oc-harness link just the plugin and instruction file, no
|
|
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
|
|
209
|
-
guard — and every one of those can be missing while the config
|
|
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
|
|
214
|
-
files had never been copied, and had `build` — the *default* agent —
|
|
215
|
-
to a `may-train` model while every generated field still matched
|
|
216
|
-
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.
|
|
217
264
|
|
|
218
|
-
So `verify` exits non-zero when the config has drifted from its profile, when
|
|
219
|
-
instruction path does not resolve, when a
|
|
220
|
-
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:
|
|
221
269
|
|
|
222
270
|
```
|
|
223
271
|
$ oc-harness verify
|
|
224
272
|
|
|
225
|
-
verify profile
|
|
273
|
+
verify profile fleet-safe -> ~/.config/opencode/opencode.jsonc
|
|
226
274
|
|
|
227
|
-
✓ config matches profile
|
|
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,
|
|
235
|
-
"the profile is fine" and "the installed config matches the profile"
|
|
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
|
-
✓
|
|
243
|
-
!
|
|
244
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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, `
|
|
338
|
-
into `~/.config/opencode/`, so this repo stays the single source
|
|
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=
|
|
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
|
|
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
|
|
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
|
-
##
|
|
508
|
+
## Lanes
|
|
406
509
|
|
|
407
|
-
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|