opencode-harness-kit 0.4.3 → 0.4.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 +527 -18
- package/CONTRIBUTING.md +242 -0
- package/GLOBAL.md +34 -5
- package/MIGRATION.md +151 -0
- package/README.md +307 -37
- package/SECURITY.md +30 -0
- package/bin/baseline.mjs +15 -7
- package/bin/git-hooks/pre-commit +41 -9
- package/bin/oc-harness.mjs +1265 -82
- package/dist/config.d.ts +127 -0
- package/dist/denylist.d.ts +33 -0
- package/dist/effort.d.ts +64 -0
- package/dist/guard.d.ts +23 -0
- package/dist/health.d.ts +43 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +532 -149
- package/dist/plugin.d.ts +15 -0
- package/dist/profile.d.ts +399 -0
- package/dist/project.d.ts +8 -0
- package/dist/prune.d.ts +6 -0
- package/dist/router.d.ts +3 -0
- package/dist/rules.d.ts +109 -0
- package/dist/safe.d.ts +7 -0
- package/dist/telemetry-store.d.ts +12 -0
- package/dist/telemetry.d.ts +3 -0
- package/docs/EXIT-CODES.md +71 -0
- package/docs/KNOWN-LIMITS.md +45 -21
- package/docs/RESEARCH.md +9 -4
- package/docs/ROUTING.md +53 -8
- package/eval/RESULTS.md +443 -0
- package/eval/ab.mjs +743 -0
- package/eval/attribute.mjs +170 -0
- package/eval/flags.mjs +88 -0
- package/eval/measure-install.mjs +323 -0
- package/eval/measure-prune.mjs +339 -0
- package/eval/metrics.js +79 -0
- package/eval/premises.mjs +200 -0
- package/eval/prepare.mjs +233 -0
- package/eval/run.mjs +376 -0
- package/eval/tasks-self.json +81 -0
- package/eval/tasks-ts.json +50 -0
- package/eval/tasks.json +83 -0
- package/eval/thresholds.mjs +134 -0
- package/eval/verify.mjs +287 -0
- package/package.json +44 -17
- package/profiles/fleet-all.json +6 -1
- package/profiles/fleet-safe.json +7 -3
- package/profiles/schema.json +182 -51
- package/prompts/build.md +4 -2
- package/skills/space-build/SKILL.md +4 -2
- package/skills/space-finish/SKILL.md +6 -2
- package/skills/space-plan/reference/task-template.md +13 -5
- package/src/config.js +182 -32
- package/src/denylist.js +95 -0
- package/src/driver/run.mjs +81 -16
- package/src/effort.js +15 -4
- package/src/guard.ts +165 -13
- package/src/health.js +80 -0
- package/src/index.ts +15 -4
- package/src/lane-usage.js +72 -18
- package/src/plugin-type-check.ts +38 -0
- package/src/plugin.ts +39 -0
- package/src/profile.js +225 -26
- package/src/project.js +47 -0
- package/src/prune.ts +110 -31
- package/src/router.ts +32 -63
- package/src/rules.js +99 -7
- package/src/safe.ts +27 -0
- package/src/sqlite.js +144 -0
- package/src/tasks.js +112 -29
- package/src/telemetry-store.ts +62 -0
- package/src/telemetry.ts +60 -57
- package/src/trust.js +70 -9
package/README.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
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
|
+
|
|
15
|
+
*It was recorded against an earlier version of the kit: the numbers it names
|
|
16
|
+
are the ones from then, and the ones below are the ones now.*
|
|
17
|
+
|
|
6
18
|
A harness kit for [OpenCode](https://opencode.ai) that is **model-agnostic by
|
|
7
19
|
construction**. Switching models is a data edit, not a code change.
|
|
8
20
|
|
|
@@ -10,6 +22,10 @@ construction**. Switching models is a data edit, not a code change.
|
|
|
10
22
|
npx -y opencode-harness-kit init # pick a free model, then install + verify
|
|
11
23
|
```
|
|
12
24
|
|
|
25
|
+
<p align="center">
|
|
26
|
+
<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">
|
|
27
|
+
</p>
|
|
28
|
+
|
|
13
29
|
then restart opencode. That is the whole quickstart. No npm account needed on
|
|
14
30
|
your side — `npx` only downloads the published kit. Prefer git? The same three
|
|
15
31
|
steps from a checkout (see [Install](#install)):
|
|
@@ -55,6 +71,13 @@ test suite asserts that rather than taking it on faith.
|
|
|
55
71
|
|
|
56
72
|
The default profile is a **fleet**: ten jobs, ten free models, one session.
|
|
57
73
|
|
|
74
|
+
The table below is the **unrestricted** fleet (`fleet-all`), where every lane
|
|
75
|
+
gets the model built for it. The default is the **safe** fleet
|
|
76
|
+
(`fleet-safe`), where every lane resolves to a provider with a zero-retention
|
|
77
|
+
policy and six of the ten run on a second-choice model to stay there. Both
|
|
78
|
+
cover the same ten jobs; they differ in what they are willing to promise
|
|
79
|
+
about your prompts.
|
|
80
|
+
|
|
58
81
|
| lane | job | model | measured |
|
|
59
82
|
|---|---|---|---|
|
|
60
83
|
| `build` | write and verify the change | MiniMax M3.1 Flash | 3.1 s |
|
|
@@ -86,11 +109,6 @@ Super VL is the only model on the tier that publishes a `none` effort level,
|
|
|
86
109
|
which makes "reasoning off" a job of its own. Putting each on the job it is
|
|
87
110
|
good at is worth more than any single "best" model.
|
|
88
111
|
|
|
89
|
-
The default is the **safe** fleet, and it is safe by construction rather than
|
|
90
|
-
by label: every lane resolves to a provider with a zero-retention policy, and
|
|
91
|
-
six of the ten lanes run on a second-choice model to stay there. The
|
|
92
|
-
unrestricted fleet gives every lane the model built for it.
|
|
93
|
-
|
|
94
112
|
The full table, with the measurements behind every choice and the three
|
|
95
113
|
capabilities the tier turns out not to have, is in
|
|
96
114
|
[`docs/ROUTING.md`](docs/ROUTING.md).
|
|
@@ -115,7 +133,7 @@ $ oc-harness use fleet-safe --print
|
|
|
115
133
|
// Derived numbers, do not hand-edit:
|
|
116
134
|
// compaction.reserved 24,000 (12% of the narrowest input cap in play)
|
|
117
135
|
// prune threshold 176,000 (narrowest input cap minus reserve)
|
|
118
|
-
// role effort plan=max build=max reviewer=max locate=
|
|
136
|
+
// role effort plan=max build=max reviewer=max locate=low ...
|
|
119
137
|
// lane models build=minimax-m3.1-flash-free (out 512,000) plan=...
|
|
120
138
|
```
|
|
121
139
|
|
|
@@ -147,7 +165,7 @@ $ oc-harness show fleet-safe
|
|
|
147
165
|
lane effort
|
|
148
166
|
plan max
|
|
149
167
|
locate low
|
|
150
|
-
sweep
|
|
168
|
+
sweep low
|
|
151
169
|
```
|
|
152
170
|
|
|
153
171
|
A model with no tunable effort at all gets **no** `variant` written, rather
|
|
@@ -199,16 +217,118 @@ it. One file covers ten jobs either way.
|
|
|
199
217
|
The five-lane profiles run everything on one model, which is the right shape
|
|
200
218
|
when you want one route and want to know exactly which one.
|
|
201
219
|
|
|
220
|
+
## Free models are perishable, so check them
|
|
221
|
+
|
|
222
|
+
Every model in the table above is a **free tier slot**, and free tier slots
|
|
223
|
+
turn over in days. Zen states free models are available "for a limited time"
|
|
224
|
+
with no date attached, so a profile is a snapshot of a moment rather than a
|
|
225
|
+
stable thing.
|
|
226
|
+
|
|
227
|
+
Observed lifetimes, from one install's 20-day history:
|
|
228
|
+
|
|
229
|
+
| model | observed lifetime |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `mimo-v2.5-free` | 1 day |
|
|
232
|
+
| `big-pickle` | 1 day |
|
|
233
|
+
| `nemotron-3-ultra-free` | 3 days |
|
|
234
|
+
| `muse-spark-1.3-contributor-free` | still running at 20 days |
|
|
235
|
+
|
|
236
|
+
The tier turned over again since. Five of this kit's own profiles are already
|
|
237
|
+
withdrawn, and one of them was the **default** at the time:
|
|
238
|
+
|
|
239
|
+
| withdrawn | why it matters |
|
|
240
|
+
|---|---|
|
|
241
|
+
| `space-bunny-free` | It was the default profile. A config left pointing at it kept working and kept saying it was zero-retention. |
|
|
242
|
+
| `x-preview-f-free` | Withdrawn within a month of shipping. |
|
|
243
|
+
| `deepseek-v4-flash-free` | Withdrawn within a month. Replaced by `deepseek-v4.1-flash-free`. |
|
|
244
|
+
| `muse-spark-1.3-contributor-free` | A Meta contributor route whose provider was permitted to train on prompts. The paid route is still in the catalogue. |
|
|
245
|
+
| `mimo-v2.5-free` | Observed alive for one day. Replaced by `mimo-v2.6-flash-free`. |
|
|
246
|
+
|
|
247
|
+
The full list, with a reason for each, is in
|
|
248
|
+
[`profiles/index.json`](profiles/index.json).
|
|
249
|
+
|
|
250
|
+
### The four commands, and when each one runs
|
|
251
|
+
|
|
252
|
+
| command | what it checks | when |
|
|
253
|
+
|---|---|---|
|
|
254
|
+
| `oc-harness list` | the profiles this kit ships, with retention and caveats | offline, any time |
|
|
255
|
+
| `oc-harness doctor` | every profile against the live model catalogue, and the installed config against the active profile | **before every release**, and whenever a lane starts failing with a 429 or a "model not found" |
|
|
256
|
+
| `oc-harness doctor --check-models` | one run, non-zero when a shipped model is no longer in the catalogue | from cron: the shape a scheduler wants |
|
|
257
|
+
| `oc-harness doctor --watch` | keeps watching, and reports only what **changed** | leave it in a terminal for a week |
|
|
258
|
+
| `oc-harness refresh <id>` | re-derives a profile's machine-derived fields from live metadata | when `doctor` reports drift |
|
|
259
|
+
| `oc-harness verify` | the installed config still matches the profile, and the kit's own files are still installed | after every `use` / `install` |
|
|
260
|
+
|
|
261
|
+
A cron line that tells you the moment a route disappears, rather than the
|
|
262
|
+
moment a lane starts failing:
|
|
263
|
+
|
|
264
|
+
```cron
|
|
265
|
+
0 9 * * * cd ~/.config/opencode && oc-harness doctor --check-models
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`doctor` separates the two kinds of finding, because they are not the same
|
|
269
|
+
kind of finding:
|
|
270
|
+
|
|
271
|
+
- **A broken install** — the config no longer matches the profile, or the
|
|
272
|
+
active profile no longer loads. This always exits non-zero.
|
|
273
|
+
- **Perishable metadata** — a profile has drifted from models.dev because a
|
|
274
|
+
provider changed a context window or a capability flag. This is reported and
|
|
275
|
+
exits zero, unless you pass `--strict`, which is for a release.
|
|
276
|
+
|
|
277
|
+
A CI job that failed on the second kind would go red on a Tuesday because
|
|
278
|
+
somebody else changed a provider's limits, and a job that goes red for a
|
|
279
|
+
reason you cannot act on is a job people stop reading.
|
|
280
|
+
|
|
281
|
+
It is not a hypothetical. The day this was written, four of the twelve
|
|
282
|
+
shipped profiles had already drifted from the live catalogue — the
|
|
283
|
+
`temperature` flag had flipped on every one of them.
|
|
284
|
+
|
|
285
|
+
### What `refresh` will and will not touch
|
|
286
|
+
|
|
287
|
+
`refresh` rewrites only the fields a machine can derive: context, output and
|
|
288
|
+
input ceilings, the effort levels a model publishes, and the capability flags.
|
|
289
|
+
It never touches `retention`, `note`, `roles` or the fallback queue, because
|
|
290
|
+
those are human judgements and a script overwriting a privacy verdict is worse
|
|
291
|
+
than drift.
|
|
292
|
+
|
|
293
|
+
**If a provider changes its terms, a human has to say so.** `retention.yml` is
|
|
294
|
+
hand-curated and dated (`verified:` on every entry). `doctor` will catch the
|
|
295
|
+
one case it can check mechanically — a profile that says `zero-retention`
|
|
296
|
+
while `retention.yml` now says `may-train` — and nothing else.
|
|
297
|
+
|
|
298
|
+
### What happens when a model is withdrawn
|
|
299
|
+
|
|
300
|
+
No plugin hook can change the model of a running session, so the failover is
|
|
301
|
+
not mid-flight:
|
|
302
|
+
|
|
303
|
+
1. The router marks the route spent when a request fails with a free-tier
|
|
304
|
+
limit or a withdrawal.
|
|
305
|
+
2. At the **next** session start, the lane that used it moves to the next rung
|
|
306
|
+
of its own chain, and the session's own model moves to the next profile in
|
|
307
|
+
the fallback queue.
|
|
308
|
+
3. A fleet keeps its lanes. One spent route costs one lane, not ten — that is
|
|
309
|
+
the whole reason every lane carries a chain of models instead of a single
|
|
310
|
+
name.
|
|
311
|
+
|
|
312
|
+
If a model is withdrawn and the lane's whole chain has gone with it, the lane
|
|
313
|
+
falls back to the profile's base model. If even that is spent, the lane still
|
|
314
|
+
gets a model and the config header says what the effective retention turned
|
|
315
|
+
out to be. A lane is never left without one.
|
|
316
|
+
|
|
317
|
+
See [docs/KNOWN-LIMITS.md](docs/KNOWN-LIMITS.md) for the rest of what this
|
|
318
|
+
tier cannot do.
|
|
319
|
+
|
|
202
320
|
## Requirements
|
|
203
321
|
|
|
204
322
|
| | |
|
|
205
323
|
|---|---|
|
|
206
324
|
| 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. |
|
|
207
325
|
| npm | 10 or newer, for `npm ci` when installing from a checkout |
|
|
208
|
-
| OS | macOS, Linux and Windows.
|
|
209
|
-
| Shell | none
|
|
210
|
-
| Network | `
|
|
211
|
-
| Disk |
|
|
326
|
+
| OS | macOS, Linux and Windows. The tests run on `ubuntu-latest`. |
|
|
327
|
+
| Shell | none needed to *use* the kit — no `bash`, `zsh` or `fish` setup. Two commands shell out to external binaries: `doctor` and `refresh` call `curl` to read models.dev, and `baseline` and the measurement scripts call `sqlite3`. Everything else, including `install`, `use` and `verify`, works offline. |
|
|
328
|
+
| Network | only `doctor` and `refresh` need it, and only to read the live model catalogue. `list` is offline: it reads the shipped profiles. Everything else works offline. |
|
|
329
|
+
| Disk | `use` and `install` write into `~/.config/opencode/` and nothing else. `doctor` writes nothing. `refresh` rewrites a profile in the kit's own
|
|
330
|
+
directory - `node_modules` for an npm install - and keeps a backup of the
|
|
331
|
+
one it replaced. |
|
|
212
332
|
|
|
213
333
|
`~/.config/opencode/` is your live agent config. `use` and `install` write there,
|
|
214
334
|
`verify` only reads, and `use --dryrun` writes nothing at all. Every write keeps
|
|
@@ -218,20 +338,40 @@ a `.bak` next to the file, including under `--yes`.
|
|
|
218
338
|
|
|
219
339
|
```
|
|
220
340
|
oc-harness list profiles with retention and caveats
|
|
221
|
-
oc-harness show <id> resolved lanes, derived numbers, fallback chains
|
|
341
|
+
oc-harness show <id> resolved lanes, derived numbers, fallback chains, guard rules
|
|
222
342
|
oc-harness init pick a free profile, then install + use + verify
|
|
223
343
|
oc-harness use <id> compile a profile into ~/.config/opencode/opencode.jsonc
|
|
224
|
-
oc-harness use <id> --dryrun print what would be written, write nothing
|
|
344
|
+
oc-harness use <id> --dryrun print a summary of what would be written, write nothing
|
|
225
345
|
oc-harness refresh <id> re-derive a profile against the live model list
|
|
226
346
|
oc-harness install symlink the plugin, prompts, skills and GLOBAL.md
|
|
227
|
-
oc-harness link just the plugin
|
|
347
|
+
oc-harness link just the plugin, no prompts or skills
|
|
228
348
|
oc-harness verify check the install against the profile; non-zero on drift
|
|
229
349
|
oc-harness agents report agents the effort tiering does not reach
|
|
230
350
|
oc-harness lanes report which lanes the fleet actually used
|
|
351
|
+
oc-harness lanes --json the same report, for a script
|
|
352
|
+
oc-harness trust set up npm trusted publishing for this package
|
|
231
353
|
oc-harness doctor re-check profiles against models.dev and the live config
|
|
354
|
+
oc-harness doctor --watch poll the catalogue, and report only what changed
|
|
355
|
+
oc-harness refresh --all re-derive every shipped profile against the live catalogue
|
|
356
|
+
oc-harness upgrade check for a newer kit, refresh the profiles, rewrite the config
|
|
357
|
+
oc-harness backups the configs that were replaced, newest first
|
|
358
|
+
oc-harness uninstall remove the plugin, prompts, skills and GLOBAL.md again
|
|
359
|
+
oc-harness cost what the kit is doing on this project, in numbers
|
|
360
|
+
oc-harness restore <file> put one back
|
|
232
361
|
oc-harness baseline measure the current install
|
|
362
|
+
oc-harness run drive .space/tasks.md to done with the build agent
|
|
233
363
|
```
|
|
234
364
|
|
|
365
|
+
Every flag accepts both spellings: `--profile fleet-safe` and
|
|
366
|
+
`--profile=fleet-safe` are the same flag.
|
|
367
|
+
|
|
368
|
+
Every command that can fail says so in its exit code, so a script can tell
|
|
369
|
+
"the kit says no" from "the kit broke".
|
|
370
|
+
[`docs/EXIT-CODES.md`](docs/EXIT-CODES.md) lists them.
|
|
371
|
+
|
|
372
|
+
`doctor`, `show` and `lanes` take `--json` and print nothing else, so a
|
|
373
|
+
script gets the answer without parsing prose.
|
|
374
|
+
|
|
235
375
|
`oc-harness <anything-else>` now exits non-zero with the list of real commands,
|
|
236
376
|
instead of printing help and reporting success.
|
|
237
377
|
|
|
@@ -312,6 +452,12 @@ A lane that is never used is a description the model reads on every turn and
|
|
|
312
452
|
a free model doing nothing. Cut it from the profile's `roles` and the lane
|
|
313
453
|
leaves the config with it.
|
|
314
454
|
|
|
455
|
+
The `build` row is not comparable with the others: it includes the turns
|
|
456
|
+
that did the delegating, so it counts the work of the other nine lanes. A
|
|
457
|
+
one-line `build` reply on its own is about a third of that number, and
|
|
458
|
+
rather more on a cold start. [`docs/ROUTING.md`](docs/ROUTING.md) has the
|
|
459
|
+
per-profile tables and what each row measures.
|
|
460
|
+
|
|
315
461
|
One request is one assistant message, which is one round trip to the provider:
|
|
316
462
|
a lane that makes three tool calls makes three requests. The counters come
|
|
317
463
|
from `.space/telemetry.jsonl`, written by the plugin, never sent anywhere, and
|
|
@@ -326,12 +472,12 @@ From one real install:
|
|
|
326
472
|
|
|
327
473
|
```
|
|
328
474
|
CONTEXT PER REQUEST
|
|
329
|
-
p50
|
|
330
|
-
fresh input p50
|
|
475
|
+
p50 99,648 p90 246,536 p99 482,779 max 547,986
|
|
476
|
+
fresh input p50 534
|
|
331
477
|
STORED MESSAGE BYTES
|
|
332
|
-
tool
|
|
333
|
-
reasoning
|
|
334
|
-
text 3.
|
|
478
|
+
tool 114.78 MB 77.9%
|
|
479
|
+
reasoning 22.94 MB 15.6%
|
|
480
|
+
text 3.04 MB 2.1%
|
|
335
481
|
READ PRESSURE
|
|
336
482
|
read calls 2,966
|
|
337
483
|
grep/glob/list 249
|
|
@@ -340,15 +486,15 @@ READ PRESSURE
|
|
|
340
486
|
cut by the 200-line cap 441
|
|
341
487
|
re-reads with an offset 423
|
|
342
488
|
PERMISSION PRESSURE
|
|
343
|
-
prompt with kit allowlist 1,002 (
|
|
489
|
+
prompt with kit allowlist 1,002 (19.1%)
|
|
344
490
|
compaction events 0 (never fired)
|
|
345
491
|
```
|
|
346
492
|
|
|
347
493
|
Four numbers in that report changed how this kit is configured, and two of them
|
|
348
494
|
reversed an earlier decision:
|
|
349
495
|
|
|
350
|
-
**Tool output is 78% of stored context**, and sessions re-read ~
|
|
351
|
-
request to add ~
|
|
496
|
+
**Tool output is 78% of stored context**, and sessions re-read ~100k tokens per
|
|
497
|
+
request to add ~534 new ones. That is why pruning here is deterministic and
|
|
352
498
|
unconditional.
|
|
353
499
|
|
|
354
500
|
**Read pressure is 11.9 whole-file reads per search.** Every production harness
|
|
@@ -364,6 +510,45 @@ reads exceed 51,200 bytes, so that ceiling is not what limits context.
|
|
|
364
510
|
|
|
365
511
|
Your numbers stay on your machine; only the script is in the repo.
|
|
366
512
|
|
|
513
|
+
`npm run measure` is an **observation** about one install: it compares the
|
|
514
|
+
work before the kit landed with the work after, and the work is never the
|
|
515
|
+
same. It says so when the two windows are not comparable, and the gate uses
|
|
516
|
+
the number that is.
|
|
517
|
+
|
|
518
|
+
`npm run measure:prune` is the attributable one. opencode stores the message
|
|
519
|
+
parts unpruned — the pruner rewrites the outgoing request, not the history —
|
|
520
|
+
so the stored parts of a real session are exactly what the pruner was given,
|
|
521
|
+
and replaying the real pruner over them measures exactly what it removed, on
|
|
522
|
+
the same messages with and without. On one install: **80.5% of the context
|
|
523
|
+
across 60 real sessions**.
|
|
524
|
+
|
|
525
|
+
## Memory
|
|
526
|
+
|
|
527
|
+
The kit does not implement memory. A local vector store with embeddings, a
|
|
528
|
+
web UI, auto-capture and a user profile is a large thing to build badly, and
|
|
529
|
+
[`opencode-mem`](https://github.com/tickernelz/opencode-mem) already is one.
|
|
530
|
+
|
|
531
|
+
What the kit does is make it one flag away and keep the config honest about
|
|
532
|
+
it:
|
|
533
|
+
|
|
534
|
+
```bash
|
|
535
|
+
oc-harness use fleet-safe --memory # adds opencode-mem to the plugin list
|
|
536
|
+
oc-harness use fleet-safe --memory=opencode-mem@2.32.0 # a pinned version
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Your own plugins are never touched — the entry is **added**, so a config that
|
|
540
|
+
already has it does not grow a second copy, and a config that already has
|
|
541
|
+
other plugins keeps them.
|
|
542
|
+
|
|
543
|
+
The memory plugin keeps its own configuration in
|
|
544
|
+
`~/.config/opencode/opencode-mem.jsonc`. It stores and searches without one,
|
|
545
|
+
but **auto-capture needs a provider** to summarise a session into a memory, so
|
|
546
|
+
give it one:
|
|
547
|
+
|
|
548
|
+
```jsonc
|
|
549
|
+
{ "opencodeProvider": "openai", "opencodeModel": "inherit" }
|
|
550
|
+
```
|
|
551
|
+
|
|
367
552
|
## The plugins
|
|
368
553
|
|
|
369
554
|
| module | hook | job |
|
|
@@ -371,7 +556,21 @@ Your numbers stay on your machine; only the script is in the repo.
|
|
|
371
556
|
| `guard.ts` | `tool.execute.before` | Blocks destructive commands in code. Fail-closed. |
|
|
372
557
|
| `prune.ts` | `experimental.chat.messages.transform` | Replaces old tool output with a one-line stub. |
|
|
373
558
|
| `router.ts` | `config`, `event`, `chat.message` | Moves a lane to the next rung of its chain when a route is spent. |
|
|
374
|
-
| `telemetry.ts` | `event`, `
|
|
559
|
+
| `telemetry.ts` | `event`, `tool.execute.after` | Local JSONL, gitignored, never a network call. |
|
|
560
|
+
|
|
561
|
+
Both `.space/telemetry.jsonl` and `.space/prune.jsonl` are written in the
|
|
562
|
+
**project**, not wherever opencode was started from. The prune log used to
|
|
563
|
+
land in the process's working directory, which is often your home directory.
|
|
564
|
+
|
|
565
|
+
**The utility route.** `small_model` is what opencode uses for its short
|
|
566
|
+
auxiliary calls — a title, a one-line summary, a question rewritten. It used
|
|
567
|
+
to be the session model, so every one of those calls paid the session model's
|
|
568
|
+
*reasoning*, and reasoning, not output, is what a short call costs. Measured
|
|
569
|
+
on this tier, a title costs **49 reasoning tokens on DeepSeek and 332 on
|
|
570
|
+
Nemotron 3.5 Super VL**, and DeepSeek answers in 7.2 s against 22.7 s for
|
|
571
|
+
GLM. A fleet now names a cheaper route; a single-model profile keeps its one
|
|
572
|
+
route, because "one route, and it is the one you asked for" is a promise those
|
|
573
|
+
profiles make. The generated file says which route it chose.
|
|
375
574
|
|
|
376
575
|
`guard.ts` matches against the command a shell would actually run, not the text
|
|
377
576
|
it was handed. It splits on shell separators, honours quotes so that a `;`
|
|
@@ -383,11 +582,19 @@ not guard-blocked; it falls through to the `*` ask permission so opencode
|
|
|
383
582
|
prompts for human approval before publishing.
|
|
384
583
|
|
|
385
584
|
Writes are scoped to the project directory, with the OS temp directory as the
|
|
386
|
-
one exception GLOBAL.md allows.
|
|
585
|
+
one exception GLOBAL.md allows. That covers every tool that writes: `write`
|
|
586
|
+
and `edit` by their path, `apply_patch` by the paths named in the patch, and
|
|
587
|
+
a bash command by its **working directory** — a command run from `/etc` is
|
|
588
|
+
writing outside the project, whatever the command is.
|
|
589
|
+
|
|
590
|
+
A write tool whose arguments name no file is blocked rather than allowed
|
|
591
|
+
through: a guard that cannot see what a write touches cannot scope it. And
|
|
592
|
+
if the guard itself cannot decide, it blocks — a guard that quietly passes
|
|
593
|
+
when broken is worse than none.
|
|
387
594
|
|
|
388
595
|
Pruning keeps user messages, assistant prose, diffs, and the last few turns
|
|
389
|
-
verbatim, and never touches the in-flight turn. Old reasoning is dropped,
|
|
390
|
-
stale reasoning is worse than none.
|
|
596
|
+
verbatim, and never touches the in-flight turn. Old reasoning is dropped,
|
|
597
|
+
since stale reasoning is worse than none.
|
|
391
598
|
|
|
392
599
|
`router.ts` fails over **per lane**. A fleet has ten routes, so one spent
|
|
393
600
|
route must not take the other nine down with it: the lane that used the dead
|
|
@@ -496,17 +703,37 @@ Writes outside the project still ask, via `external_directory`.
|
|
|
496
703
|
|
|
497
704
|
| lane | mode | job | effort |
|
|
498
705
|
|---|---|---|---|
|
|
499
|
-
| `build` | primary | writes and verifies code |
|
|
500
|
-
| `plan` | primary | turns a request into an executable plan |
|
|
501
|
-
| `reviewer` | subagent | two passes, never edits |
|
|
502
|
-
| `locate` | subagent | finds code, reports `file:line`, never designs |
|
|
503
|
-
| `verify` | subagent | runs checks, never fixes them |
|
|
706
|
+
| `build` | primary | writes and verifies code | `max` |
|
|
707
|
+
| `plan` | primary | turns a request into an executable plan | `max` |
|
|
708
|
+
| `reviewer` | subagent | two passes, never edits | `max` |
|
|
709
|
+
| `locate` | subagent | finds code, reports `file:line`, never designs | `low` |
|
|
710
|
+
| `verify` | subagent | runs checks, never fixes them | `low` |
|
|
504
711
|
| `vision` | subagent | reads an image or a screenshot | model's |
|
|
505
712
|
| `doc` | subagent | reads a long document or a spec | model's |
|
|
506
|
-
| `schema` | subagent | produces JSON a program can parse |
|
|
507
|
-
| `sweep` | subagent | bulk mechanical sweep, no judgement |
|
|
713
|
+
| `schema` | subagent | produces JSON a program can parse | `max` |
|
|
714
|
+
| `sweep` | subagent | bulk mechanical sweep, no judgement | `low` / `none` |
|
|
508
715
|
| `longform` | subagent | very long context, or several answers | model's |
|
|
509
716
|
|
|
717
|
+
The effort column is the request in the profile, resolved against the levels
|
|
718
|
+
of the model the lane actually runs on — so `max` is `max` where the model
|
|
719
|
+
publishes it and the nearest level that does exist where it does not, and
|
|
720
|
+
`none` is no variant at all on a model that cannot turn reasoning off. Run
|
|
721
|
+
`oc-harness show <profile>` for the resolved values and the note explaining
|
|
722
|
+
any difference.
|
|
723
|
+
|
|
724
|
+
The tier is worth protecting: measured on one install, DeepSeek spends **320
|
|
725
|
+
reasoning tokens a message at `max` and 19 at `low`**, so the lever is a
|
|
726
|
+
factor of 17. A lane that asks for a tier and lands on a model that publishes
|
|
727
|
+
none runs at that model's natural rate instead, and the generated config says
|
|
728
|
+
so in the form `locate=unset(asked low)`.
|
|
729
|
+
|
|
730
|
+
Three lanes in the unrestricted fleet cannot get their tier, and the
|
|
731
|
+
measurement says they are the three cheapest reasoners on the tier — 116, 59
|
|
732
|
+
and 18 reasoning tokens a message, against 289 for `build` and 198 for
|
|
733
|
+
`plan`, which keep their tier on purpose. The one lane that was genuinely
|
|
734
|
+
expensive was `sweep`, running on a model that reasons 420 tokens a message
|
|
735
|
+
for what is supposed to be a mechanical grep. It does not any more.
|
|
736
|
+
|
|
510
737
|
`locate` is deliberately *not* called `scout`: OpenCode ships a built-in `scout`
|
|
511
738
|
subagent for dependency and external-docs research, and shipping our own file
|
|
512
739
|
under that name shadowed it with `bash: deny` so it could not do the one thing
|
|
@@ -516,6 +743,11 @@ Each lane carries a `steps` cap. It is a loop breaker, not a quality knob: on
|
|
|
516
743
|
exhaustion OpenCode forces a text-only response, so an agent that has stopped
|
|
517
744
|
making progress reports rather than looping until the context is gone.
|
|
518
745
|
|
|
746
|
+
Each lane also gets its own `permission.task` rule — `deny` for everything
|
|
747
|
+
except the lanes it may hand work to. That is the mechanism behind "hand the
|
|
748
|
+
job to the lane that has it": a lane that is not allowed to spawn anything
|
|
749
|
+
cannot quietly become a general-purpose agent on the most expensive tier.
|
|
750
|
+
|
|
519
751
|
## The skill loop
|
|
520
752
|
|
|
521
753
|
The lanes above are the *who*. The skills are the *order*, and they load by
|
|
@@ -537,6 +769,43 @@ the conversation instead of the requirement.
|
|
|
537
769
|
Publishing is never a loop step. `space-finish` prepares the change; a human
|
|
538
770
|
decides it ships.
|
|
539
771
|
|
|
772
|
+
## What it costs, measured
|
|
773
|
+
|
|
774
|
+
The kit's own A/B - the same task with and without it, ten reps per arm, 120
|
|
775
|
+
agent runs, both arms on the same model, on this repository at a pinned commit
|
|
776
|
+
so anyone can repeat it:
|
|
777
|
+
|
|
778
|
+
| metric (median) | kit on | kit off |
|
|
779
|
+
|---|---|---|
|
|
780
|
+
| **correct** | **54/60** | **55/60** |
|
|
781
|
+
| tool calls | 12.5 | 8 |
|
|
782
|
+
| input tokens | 56,205 | 10,540 |
|
|
783
|
+
| final context | 39,928 | 12,718 |
|
|
784
|
+
| wall seconds | 43 | 21 |
|
|
785
|
+
| paths written outside the target | 0 | 0 |
|
|
786
|
+
|
|
787
|
+
**On a task that fits in one read, the kit is a cost.** It spends over five
|
|
788
|
+
times the input tokens and takes twice as long, and the answers are a tie -
|
|
789
|
+
54 against 55, and a second run of the same 120 runs gave 55 against 54.
|
|
790
|
+
The two edit tasks are a tie at 10/10, and the guard stopped no writes in
|
|
791
|
+
either arm.
|
|
792
|
+
|
|
793
|
+
The earlier version of this table said 52/60 against 59/59 - the kit looked
|
|
794
|
+
clearly worse - and that was the grader's fault: it could not read a
|
|
795
|
+
citation written as `**File:** x` on one line and `**Line:** 105` on the
|
|
796
|
+
next, which is the shape some of the kit's answers took. Re-scoring the
|
|
797
|
+
same 120 runs with the fixed grader gives 34/40 against 30/40 on the lookup
|
|
798
|
+
tasks - the kit ahead, where the old grader had it behind. The full account is in [eval/RESULTS.md](eval/RESULTS.md).
|
|
799
|
+
|
|
800
|
+
What the kit buys is in the two places that suite cannot see: **long
|
|
801
|
+
sessions**, where the pruner removes 80.5% of the context, and **process** -
|
|
802
|
+
a reviewer that reviews and does not edit, a verify that runs the checks and
|
|
803
|
+
does not fix them, a publish decision that belongs to a human.
|
|
804
|
+
|
|
805
|
+
Whether it is worth it depends on whether the work is long. A repository of
|
|
806
|
+
one-file lookups should not install this. The full table is in
|
|
807
|
+
[eval/RESULTS.md](eval/RESULTS.md).
|
|
808
|
+
|
|
540
809
|
## Known limits
|
|
541
810
|
|
|
542
811
|
Read [docs/KNOWN-LIMITS.md](docs/KNOWN-LIMITS.md) before relying on failover.
|
|
@@ -549,7 +818,7 @@ session starts.
|
|
|
549
818
|
|
|
550
819
|
```bash
|
|
551
820
|
npm install
|
|
552
|
-
npm run check # everything: build, typecheck, tests
|
|
821
|
+
npm run check # everything offline: build, typecheck, tests
|
|
553
822
|
```
|
|
554
823
|
|
|
555
824
|
Or one layer at a time:
|
|
@@ -557,6 +826,7 @@ Or one layer at a time:
|
|
|
557
826
|
```bash
|
|
558
827
|
npm test # model-agnostic assertions, no network, no model calls
|
|
559
828
|
npm run typecheck
|
|
829
|
+
npm run check:install # the above, plus the live install measurement and verify
|
|
560
830
|
npm run eval # synthetic: pruning, prompt pressure, effort projection
|
|
561
831
|
npm run measure # is this install behaving? narrow reads, context
|
|
562
832
|
npm run attribute # where the context went: fixed overhead vs history
|
|
@@ -565,8 +835,8 @@ npm run baseline # your own install, from scratch
|
|
|
565
835
|
```
|
|
566
836
|
|
|
567
837
|
`eval/ab.mjs` runs the same tasks with and without the kit, to separate the kit
|
|
568
|
-
from the work. It spawns real agents, so it is not part of
|
|
569
|
-
|
|
838
|
+
from the work. It spawns real agents, so it is not part of any check, and it
|
|
839
|
+
wants a subject it can hand each run a private copy of:
|
|
570
840
|
|
|
571
841
|
```bash
|
|
572
842
|
node eval/prepare.mjs --repo ~/work/lablab-harness --ref 8a674f5
|
|
@@ -576,7 +846,7 @@ node eval/ab.mjs --rescore <saved stream dir> # re-grade without spawning agen
|
|
|
576
846
|
|
|
577
847
|
`npm test` and `npm run typecheck` need nothing but the repo, so CI runs those.
|
|
578
848
|
The rest read `~/.local/share/opencode/opencode.db` and `~/.config/opencode`,
|
|
579
|
-
which only exist on a machine that has run OpenCode with the kit installed
|
|
849
|
+
which only exist on a machine that has run OpenCode with the kit installed —
|
|
580
850
|
`npm run check:online` additionally hits the model catalog over the network.
|
|
581
851
|
|
|
582
852
|
Measured results, and the limits they do not cross, are in
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
|---|---|
|
|
7
|
+
| 0.4.x | yes |
|
|
8
|
+
| 0.3.x | yes |
|
|
9
|
+
| 0.2.x | security fixes only |
|
|
10
|
+
| 0.1.x | security fixes only |
|
|
11
|
+
|
|
12
|
+
## Reporting a vulnerability
|
|
13
|
+
|
|
14
|
+
Please report privately through GitHub's
|
|
15
|
+
[private vulnerability reporting](https://github.com/pham-canh/space-harness/security/advisories/new)
|
|
16
|
+
rather than opening a public issue.
|
|
17
|
+
|
|
18
|
+
Include the affected version, a reproduction, and the expected behaviour.
|
|
19
|
+
You can expect an acknowledgement within 72 hours and a fix or mitigation
|
|
20
|
+
plan within 7 days.
|
|
21
|
+
|
|
22
|
+
## Scope
|
|
23
|
+
|
|
24
|
+
`space-harness` runs agent subprocesses and reads harness config. Reports
|
|
25
|
+
that are in scope include path traversal when resolving a capability
|
|
26
|
+
profile, command injection through harness config values, and any way a
|
|
27
|
+
harness writes outside its configured working directory.
|
|
28
|
+
|
|
29
|
+
Out of scope: anything requiring you to hand over a credential you were
|
|
30
|
+
never given, and issues in the model provider itself.
|
package/bin/baseline.mjs
CHANGED
|
@@ -19,18 +19,24 @@ const wantJson = args.includes("--json")
|
|
|
19
19
|
const dbArg = args.indexOf("--db")
|
|
20
20
|
const DB = dbArg >= 0 ? args[dbArg + 1] : join(homedir(), ".local/share/opencode/opencode.db")
|
|
21
21
|
|
|
22
|
+
const { canQuerySqlite, sqliteMissingMessage, query: queryDb } = await import("../src/sqlite.js")
|
|
23
|
+
|
|
22
24
|
if (!existsSync(DB)) {
|
|
23
25
|
console.error(`opencode database not found: ${DB}`)
|
|
24
26
|
console.error("pass --db <path> or run opencode at least once first")
|
|
25
27
|
process.exit(1)
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
// The sqlite3 CLI, not the library. A missing tool is a thing a person can
|
|
31
|
+
// fix, and the failure used to be a raw `spawnSync sqlite3 ENOENT` stack
|
|
32
|
+
// trace.
|
|
33
|
+
if (!canQuerySqlite()) {
|
|
34
|
+
console.error(sqliteMissingMessage())
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
function query(sql) {
|
|
29
|
-
|
|
30
|
-
encoding: "utf8",
|
|
31
|
-
maxBuffer: 256 * 1024 * 1024,
|
|
32
|
-
}).trim()
|
|
33
|
-
return out ? JSON.parse(out) : []
|
|
39
|
+
return queryDb(DB, sql)
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
const ASSISTANT = `json_extract(data,'$.role')='assistant'`
|
|
@@ -163,7 +169,9 @@ const topAskers = () => {
|
|
|
163
169
|
// allowlist, which meant the number it reported was whatever the list looked
|
|
164
170
|
// like when the copy was last edited. Every tuning decision in this project is
|
|
165
171
|
// supposed to rest on this measurement, so it has to measure the real list.
|
|
166
|
-
const { BASH_ALLOWLIST } = await import(
|
|
172
|
+
const { BASH_ALLOWLIST } = await import(
|
|
173
|
+
new URL("../src/config.js", import.meta.url).href,
|
|
174
|
+
)
|
|
167
175
|
const kitAllowlist = BASH_ALLOWLIST
|
|
168
176
|
const currentAllow = [
|
|
169
177
|
"git status *", "git log *", "git diff *", "grep *",
|
|
@@ -184,7 +192,7 @@ const paidEquivalent =
|
|
|
184
192
|
//
|
|
185
193
|
// This exists because the kit once shipped max_lines 200 / max_bytes 8192 on
|
|
186
194
|
// the reasoning that tight truncation "saves context", and a later install
|
|
187
|
-
// measured 415 of 2,
|
|
195
|
+
// measured 415 of 2,966 reads exceeding 8 KB against 423 reads issued with an
|
|
188
196
|
// `offset`. Truncation causes the re-read roughly one for one, and at the
|
|
189
197
|
// measured ~17s median per request that round trip is more expensive than the
|
|
190
198
|
// tokens it saves. Re-run this before changing these numbers again.
|
package/bin/git-hooks/pre-commit
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
#!/bin/sh
|
|
2
2
|
# Installed by `oc-harness install`. Remove with: rm .git/hooks/pre-commit
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
# left out: measure and attribute read ~/.local/share/opencode, which is slow,
|
|
6
|
-
# machine-specific, and irrelevant to whether a commit broke the kit.
|
|
4
|
+
# It runs the PROJECT's own checks, and only if the project has any.
|
|
7
5
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# The previous version ran `npm run test:all`, which is a script of THIS KIT,
|
|
7
|
+
# not of the project the hook was installed into. So in every project that
|
|
8
|
+
# installed the kit, every commit failed with:
|
|
9
|
+
#
|
|
10
|
+
# npm error Missing script: "test:all"
|
|
11
|
+
#
|
|
12
|
+
# until the author reached for --no-verify. A gate that blocks every commit
|
|
13
|
+
# is not a gate.
|
|
14
|
+
#
|
|
15
|
+
# The kit's own repository is the exception, and it is handled by the script
|
|
16
|
+
# list below: `test:all` is there, so the kit gates itself.
|
|
17
|
+
#
|
|
18
|
+
# The live-install half is deliberately left out: measure and attribute read
|
|
19
|
+
# ~/.local/share/opencode, which is slow, machine-specific, and irrelevant to
|
|
20
|
+
# whether a commit broke anything.
|
|
10
21
|
|
|
11
22
|
set -e
|
|
12
23
|
|
|
@@ -18,13 +29,34 @@ if [ -n "$SKIP_SPACE_HARNESS_HOOK" ]; then
|
|
|
18
29
|
exit 0
|
|
19
30
|
fi
|
|
20
31
|
|
|
21
|
-
|
|
22
|
-
if !
|
|
32
|
+
# No package.json, no JavaScript project, no gate.
|
|
33
|
+
if [ ! -f package.json ]; then
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# The gate is the project's own test script, if it has one. The kit's own
|
|
38
|
+
# repository has `test:all`, which is the offline half of its gate.
|
|
39
|
+
gate=""
|
|
40
|
+
if node -e "process.exit(require('./package.json').scripts?.['test:all'] ? 0 : 1)" 2>/dev/null; then
|
|
41
|
+
gate="test:all"
|
|
42
|
+
elif node -e "process.exit(require('./package.json').scripts?.test ? 0 : 1)" 2>/dev/null; then
|
|
43
|
+
gate="test"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
if [ -z "$gate" ]; then
|
|
47
|
+
# Nothing to run. A hook that blocks every commit because the project has
|
|
48
|
+
# no test script is worse than no hook.
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
log="${TMPDIR:-/tmp}/space-harness-hook.log"
|
|
53
|
+
echo "pre-commit: npm run $gate"
|
|
54
|
+
if ! npm run "$gate" >"$log" 2>&1; then
|
|
23
55
|
echo "pre-commit: FAILED. Last lines:"
|
|
24
|
-
tail -30
|
|
56
|
+
tail -30 "$log"
|
|
25
57
|
echo ""
|
|
26
58
|
echo "Fix it, or commit with --no-verify if you are certain it is not yours."
|
|
27
59
|
exit 1
|
|
28
60
|
fi
|
|
29
61
|
|
|
30
|
-
exit 0
|
|
62
|
+
exit 0
|