opencode-harness-kit 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +569 -18
  2. package/CONTRIBUTING.md +249 -0
  3. package/GLOBAL.md +34 -5
  4. package/MIGRATION.md +151 -0
  5. package/README.md +354 -66
  6. package/SECURITY.md +30 -0
  7. package/bin/baseline.mjs +15 -7
  8. package/bin/git-hooks/pre-commit +41 -9
  9. package/bin/oc-harness.mjs +1268 -82
  10. package/dist/config.d.ts +127 -0
  11. package/dist/denylist.d.ts +33 -0
  12. package/dist/effort.d.ts +64 -0
  13. package/dist/guard.d.ts +23 -0
  14. package/dist/health.d.ts +43 -0
  15. package/dist/index.d.ts +9 -0
  16. package/dist/index.js +532 -149
  17. package/dist/plugin.d.ts +15 -0
  18. package/dist/profile.d.ts +399 -0
  19. package/dist/project.d.ts +8 -0
  20. package/dist/prune.d.ts +6 -0
  21. package/dist/router.d.ts +3 -0
  22. package/dist/rules.d.ts +109 -0
  23. package/dist/safe.d.ts +7 -0
  24. package/dist/telemetry-store.d.ts +12 -0
  25. package/dist/telemetry.d.ts +3 -0
  26. package/docs/EXIT-CODES.md +83 -0
  27. package/docs/KNOWN-LIMITS.md +45 -21
  28. package/docs/RESEARCH.md +20 -5
  29. package/docs/ROUTING.md +191 -31
  30. package/eval/RESULTS.md +457 -0
  31. package/eval/ab.mjs +743 -0
  32. package/eval/attribute.mjs +170 -0
  33. package/eval/flags.mjs +88 -0
  34. package/eval/measure-install.mjs +331 -0
  35. package/eval/measure-prune.mjs +339 -0
  36. package/eval/metrics.js +79 -0
  37. package/eval/models.mjs +302 -0
  38. package/eval/premises.mjs +200 -0
  39. package/eval/prepare.mjs +233 -0
  40. package/eval/run.mjs +376 -0
  41. package/eval/tasks-self.json +81 -0
  42. package/eval/tasks-ts.json +50 -0
  43. package/eval/tasks.json +83 -0
  44. package/eval/thresholds.mjs +134 -0
  45. package/eval/verify.mjs +287 -0
  46. package/package.json +44 -17
  47. package/profiles/fleet-all.json +6 -1
  48. package/profiles/fleet-safe.json +7 -3
  49. package/profiles/schema.json +182 -51
  50. package/prompts/build.md +4 -2
  51. package/skills/space-build/SKILL.md +4 -2
  52. package/skills/space-finish/SKILL.md +6 -2
  53. package/skills/space-plan/reference/task-template.md +13 -5
  54. package/src/config.js +182 -32
  55. package/src/denylist.js +95 -0
  56. package/src/driver/run.mjs +81 -16
  57. package/src/effort.js +15 -4
  58. package/src/guard.ts +165 -13
  59. package/src/health.js +80 -0
  60. package/src/index.ts +15 -4
  61. package/src/lane-usage.js +72 -18
  62. package/src/plugin-type-check.ts +38 -0
  63. package/src/plugin.ts +39 -0
  64. package/src/profile.js +225 -26
  65. package/src/project.js +47 -0
  66. package/src/prune.ts +110 -31
  67. package/src/router.ts +32 -63
  68. package/src/rules.js +99 -7
  69. package/src/safe.ts +27 -0
  70. package/src/sqlite.js +144 -0
  71. package/src/tasks.js +112 -29
  72. package/src/telemetry-store.ts +62 -0
  73. package/src/telemetry.ts +60 -57
  74. package/src/trust.js +70 -9
package/README.md CHANGED
@@ -3,6 +3,18 @@
3
3
  [![CI](https://github.com/pham-canh/space-harness/actions/workflows/ci.yml/badge.svg)](https://github.com/pham-canh/space-harness/actions/workflows/ci.yml)
4
4
  [![npm](https://img.shields.io/npm/v/opencode-harness-kit.svg)](https://www.npmjs.com/package/opencode-harness-kit)
5
5
 
6
+ <p align="center">
7
+ <video src="docs/demo/pitch.mp4" width="720" controls></video>
8
+ </p>
9
+
10
+ *Four minutes forty-seven seconds, recorded live from a real terminal and
11
+ narrated. The take script and the voiceover are in [`video/`](video/), and
12
+ rebuilding it is documented there — including the two mistakes that cost the
13
+ most time, so the next person does not repeat them.*
14
+
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,18 +71,31 @@ 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
- | `build` | write and verify the change | MiniMax M3.1 Flash | 3.1 s |
61
- | `plan` | turn a request into a plan | Beam | **2.6 s** |
62
- | `reviewer` | read a change in two passes | Nemotron 3 Ultra | 4.7 s (34 s cold) |
63
- | `locate` | find where something lives | Nemotron 3.5 Lightning | 3.7 s |
64
- | `verify` | run the project's checks | Big Pickle | 6.5 s |
65
- | `vision` | read an image or a screenshot | MiMo-V2.6 Flash | 19.2 s |
66
- | `doc` | read a long document or a spec | GLM 5.3 Flash | 5.4 s |
67
- | `schema` | produce JSON or a JSON schema | DeepSeek V4.1 Flash | 3.4 s |
68
- | `sweep` | sweep a lot of code mechanically | Nemotron 3.5 Super VL | 4.2 s |
69
- | `longform` | read a megabyte, or write alternatives | LongCat 2.5 Preview | 9.4 s |
83
+ | `build` | write and verify the change | MiniMax M3.1 Flash | 6.9 s |
84
+ | `plan` | turn a request into a plan | Beam | **6.1 s** |
85
+ | `reviewer` | read a change in two passes | Nemotron 3 Ultra | 7.8 s (34 s cold) |
86
+ | `locate` | find where something lives | Nemotron 3.5 Lightning | 6.9 s |
87
+ | `verify` | run the project's checks | Big Pickle | 10.4 s |
88
+ | `vision` | read an image or a screenshot | MiMo-V2.6 Flash | 9.3 s (19 s with an image) |
89
+ | `doc` | read a long document or a spec | GLM 5.3 Flash | 7.4 s |
90
+ | `schema` | produce JSON or a JSON schema | DeepSeek V4.1 Flash | 10.7 s |
91
+ | `sweep` | sweep a lot of code mechanically | Nemotron 3.5 Super VL | 8.4 s |
92
+ | `longform` | read a megabyte, or write alternatives | LongCat 2.5 Preview | 12.0 s |
93
+
94
+ The `measured` column is the median of five one-line requests to that model
95
+ on 2026-10-21 - the same prompt for every model, from the process start, so
96
+ it is the wait a lane would cost.
97
+ [`docs/ROUTING.md`](docs/ROUTING.md) has the method, the install telemetry
98
+ beside it, and the command that produced it.
70
99
 
71
100
  Each lane is an OpenCode agent with its own model, its own context window and
72
101
  its own effort level. You do not switch models: you hand the job to the lane
@@ -80,16 +109,12 @@ agent=build modelID=minimax-m3.1-flash-free
80
109
  ```
81
110
 
82
111
  Why ten lanes and not one model: the free tier is genuinely uneven, and the
83
- difference is measurable. Beam answers in 2.6 seconds with a 1M window;
84
- MiMo reads an image in 19 and MiniMax reads the same image in 8; Nemotron
85
- Super VL is the only model on the tier that publishes a `none` effort level,
86
- which makes "reasoning off" a job of its own. Putting each on the job it is
87
- good at is worth more than any single "best" model.
88
-
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.
112
+ difference is measurable. Beam answers in 6.1 seconds with a 1M window, and
113
+ the four fastest models are inside a second of each other; MiMo reads an
114
+ image in 19 and MiniMax reads the same image in 8; Nemotron Super VL is the
115
+ only model on the tier that publishes a `none` effort level, which makes
116
+ "reasoning off" a job of its own. Putting each on the job it is good at is
117
+ worth more than any single "best" model.
93
118
 
94
119
  The full table, with the measurements behind every choice and the three
95
120
  capabilities the tier turns out not to have, is in
@@ -115,7 +140,7 @@ $ oc-harness use fleet-safe --print
115
140
  // Derived numbers, do not hand-edit:
116
141
  // compaction.reserved 24,000 (12% of the narrowest input cap in play)
117
142
  // prune threshold 176,000 (narrowest input cap minus reserve)
118
- // role effort plan=max build=max reviewer=max locate=unset ...
143
+ // role effort plan=max build=max reviewer=max locate=low ...
119
144
  // lane models build=minimax-m3.1-flash-free (out 512,000) plan=...
120
145
  ```
121
146
 
@@ -147,7 +172,7 @@ $ oc-harness show fleet-safe
147
172
  lane effort
148
173
  plan max
149
174
  locate low
150
- sweep (unset) (requested none)
175
+ sweep low
151
176
  ```
152
177
 
153
178
  A model with no tunable effort at all gets **no** `variant` written, rather
@@ -199,39 +224,173 @@ it. One file covers ten jobs either way.
199
224
  The five-lane profiles run everything on one model, which is the right shape
200
225
  when you want one route and want to know exactly which one.
201
226
 
227
+ ## Free models are perishable, so check them
228
+
229
+ Every model in the table above is a **free tier slot**, and free tier slots
230
+ turn over in days. Zen states free models are available "for a limited time"
231
+ with no date attached, so a profile is a snapshot of a moment rather than a
232
+ stable thing.
233
+
234
+ Observed lifetimes, from one install's 20-day history:
235
+
236
+ | model | observed lifetime |
237
+ |---|---|
238
+ | `mimo-v2.5-free` | 1 day |
239
+ | `big-pickle` | 1 day |
240
+ | `nemotron-3-ultra-free` | 3 days |
241
+ | `muse-spark-1.3-contributor-free` | still running at 20 days |
242
+
243
+ The tier turned over again since. Five of this kit's own profiles are already
244
+ withdrawn, and one of them was the **default** at the time:
245
+
246
+ | withdrawn | why it matters |
247
+ |---|---|
248
+ | `space-bunny-free` | It was the default profile. A config left pointing at it kept working and kept saying it was zero-retention. |
249
+ | `x-preview-f-free` | Withdrawn within a month of shipping. |
250
+ | `deepseek-v4-flash-free` | Withdrawn within a month. Replaced by `deepseek-v4.1-flash-free`. |
251
+ | `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. |
252
+ | `mimo-v2.5-free` | Observed alive for one day. Replaced by `mimo-v2.6-flash-free`. |
253
+
254
+ The full list, with a reason for each, is in
255
+ [`profiles/index.json`](profiles/index.json).
256
+
257
+ ### The four commands, and when each one runs
258
+
259
+ | command | what it checks | when |
260
+ |---|---|---|
261
+ | `oc-harness list` | the profiles this kit ships, with retention and caveats | offline, any time |
262
+ | `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" |
263
+ | `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 |
264
+ | `oc-harness doctor --watch` | keeps watching, and reports only what **changed** | leave it in a terminal for a week |
265
+ | `oc-harness refresh <id>` | re-derives a profile's machine-derived fields from live metadata | when `doctor` reports drift |
266
+ | `oc-harness verify` | the installed config still matches the profile, and the kit's own files are still installed | after every `use` / `install` |
267
+
268
+ A cron line that tells you the moment a route disappears, rather than the
269
+ moment a lane starts failing:
270
+
271
+ ```cron
272
+ 0 9 * * * cd ~/.config/opencode && oc-harness doctor --check-models
273
+ ```
274
+
275
+ `doctor` separates the two kinds of finding, because they are not the same
276
+ kind of finding:
277
+
278
+ - **A broken install** — the config no longer matches the profile, or the
279
+ active profile no longer loads. This always exits non-zero.
280
+ - **Perishable metadata** — a profile has drifted from models.dev because a
281
+ provider changed a context window or a capability flag. This is reported and
282
+ exits zero, unless you pass `--strict`, which is for a release.
283
+
284
+ A CI job that failed on the second kind would go red on a Tuesday because
285
+ somebody else changed a provider's limits, and a job that goes red for a
286
+ reason you cannot act on is a job people stop reading.
287
+
288
+ It is not a hypothetical. The day this was written, four of the twelve
289
+ shipped profiles had already drifted from the live catalogue — the
290
+ `temperature` flag had flipped on every one of them.
291
+
292
+ ### What `refresh` will and will not touch
293
+
294
+ `refresh` rewrites only the fields a machine can derive: context, output and
295
+ input ceilings, the effort levels a model publishes, and the capability flags.
296
+ It never touches `retention`, `note`, `roles` or the fallback queue, because
297
+ those are human judgements and a script overwriting a privacy verdict is worse
298
+ than drift.
299
+
300
+ **If a provider changes its terms, a human has to say so.** `retention.yml` is
301
+ hand-curated and dated (`verified:` on every entry). `doctor` will catch the
302
+ one case it can check mechanically — a profile that says `zero-retention`
303
+ while `retention.yml` now says `may-train` — and nothing else.
304
+
305
+ ### What happens when a model is withdrawn
306
+
307
+ No plugin hook can change the model of a running session, so the failover is
308
+ not mid-flight:
309
+
310
+ 1. The router marks the route spent when a request fails with a free-tier
311
+ limit or a withdrawal.
312
+ 2. At the **next** session start, the lane that used it moves to the next rung
313
+ of its own chain, and the session's own model moves to the next profile in
314
+ the fallback queue.
315
+ 3. A fleet keeps its lanes. One spent route costs one lane, not ten — that is
316
+ the whole reason every lane carries a chain of models instead of a single
317
+ name.
318
+
319
+ If a model is withdrawn and the lane's whole chain has gone with it, the lane
320
+ falls back to the profile's base model. If even that is spent, the lane still
321
+ gets a model and the config header says what the effective retention turned
322
+ out to be. A lane is never left without one.
323
+
324
+ See [docs/KNOWN-LIMITS.md](docs/KNOWN-LIMITS.md) for the rest of what this
325
+ tier cannot do.
326
+
202
327
  ## Requirements
203
328
 
204
329
  | | |
205
330
  |---|---|
206
331
  | 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
332
  | npm | 10 or newer, for `npm ci` when installing from a checkout |
208
- | OS | macOS, Linux and Windows. No platform checks in the code; the tests run on `ubuntu-latest`. |
209
- | Shell | none. The CLI never shells out, so no `bash`, `zsh` or `fish` setup is needed. |
210
- | Network | `list` and `doctor` read models.dev, which is the point of both. Everything else, including `install`, `use` and `verify`, works offline. |
211
- | Disk | it writes into `~/.config/opencode/` and nothing else. |
333
+ | OS | macOS, Linux and Windows. The tests run on `ubuntu-latest`. |
334
+ | Shell | none needed to *use* the kit — no `bash`, `zsh` or `fish` setup. `doctor` and `refresh` call `curl` to read models.dev. `baseline` and the measurement scripts read the install database with `sqlite3`, and fall back to python3's own `sqlite3` module when that binary is not installed — so python3 is the one thing to have. Everything else, including `install`, `use` and `verify`, works offline. |
335
+ | 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. |
336
+ | Disk | `use` and `install` write into `~/.config/opencode/` and nothing else. `doctor` writes nothing. `refresh` rewrites a profile in the kit's own
337
+ directory - `node_modules` for an npm install - and keeps a backup of the
338
+ one it replaced. |
212
339
 
213
340
  `~/.config/opencode/` is your live agent config. `use` and `install` write there,
214
- `verify` only reads, and `use --dryrun` writes nothing at all. Every write keeps
215
- a `.bak` next to the file, including under `--yes`.
341
+ `verify` only reads, and `use --dryrun` writes nothing at all. Every write
342
+ keeps a dated copy in `~/.config/opencode/backups/`, including under
343
+ `--yes`, and `oc-harness restore <file>` puts one back.
216
344
 
217
345
  ## Commands
218
346
 
219
347
  ```
220
348
  oc-harness list profiles with retention and caveats
221
- oc-harness show <id> resolved lanes, derived numbers, fallback chains
349
+ oc-harness show <id> resolved lanes, derived numbers, fallback chains, guard rules
222
350
  oc-harness init pick a free profile, then install + use + verify
223
351
  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
352
+ oc-harness use <id> --dryrun print a summary of what would be written, write nothing
225
353
  oc-harness refresh <id> re-derive a profile against the live model list
226
354
  oc-harness install symlink the plugin, prompts, skills and GLOBAL.md
227
- oc-harness link just the plugin and instruction file, no prompts or skills
355
+ oc-harness link just the plugin, no prompts or skills
228
356
  oc-harness verify check the install against the profile; non-zero on drift
229
357
  oc-harness agents report agents the effort tiering does not reach
230
358
  oc-harness lanes report which lanes the fleet actually used
359
+ oc-harness lanes --json the same report, for a script
360
+ oc-harness trust set up npm trusted publishing for this package
231
361
  oc-harness doctor re-check profiles against models.dev and the live config
362
+ oc-harness doctor --watch poll the catalogue, and report only what changed
363
+ oc-harness refresh --all re-derive every shipped profile against the live catalogue
364
+ oc-harness upgrade check for a newer kit, refresh the profiles, rewrite the config
365
+ oc-harness backups the configs that were replaced, newest first
366
+ oc-harness uninstall remove the plugin, prompts, skills and GLOBAL.md again
367
+ oc-harness cost what the kit is doing on this project, in numbers
368
+ oc-harness restore <file> put one back
232
369
  oc-harness baseline measure the current install
370
+ oc-harness run drive .space/tasks.md to done with the build agent
371
+ ```
372
+
373
+ The measurement scripts are in `eval/`, and each one says what it measures
374
+ at the top of the file:
375
+
376
+ ```
377
+ node eval/run.mjs the pruner and the allowlist
378
+ node eval/measure-prune.mjs the pruner, on real sessions
379
+ node eval/measure-install.mjs before and after the install
380
+ node eval/models.mjs every model the profiles use, one line each
381
+ node eval/ab.mjs the A/B: the same task with and without the kit
233
382
  ```
234
383
 
384
+ Every flag accepts both spellings: `--profile fleet-safe` and
385
+ `--profile=fleet-safe` are the same flag.
386
+
387
+ Every command that can fail says so in its exit code, so a script can tell
388
+ "the kit says no" from "the kit broke".
389
+ [`docs/EXIT-CODES.md`](docs/EXIT-CODES.md) lists them.
390
+
391
+ `doctor`, `show` and `lanes` take `--json` and print nothing else, so a
392
+ script gets the answer without parsing prose.
393
+
235
394
  `oc-harness <anything-else>` now exits non-zero with the list of real commands,
236
395
  instead of printing help and reporting success.
237
396
 
@@ -294,24 +453,26 @@ Ten lanes is a hypothesis. `oc-harness lanes` counts the requests:
294
453
  ```
295
454
  $ oc-harness lanes
296
455
 
297
- lane reqs avg ms model
298
- ----------------------------------------------------------------
299
- plan 7 2624 beam-free
300
- build 21 10104 minimax-m3.1-flash-free
301
- reviewer 2 4716 nemotron-3-ultra-free
302
- locate 4 3680 nemotron-3.5-lightning-free
303
- verify 2 6546 big-pickle
304
- vision 2 19167 mimo-v2.6-flash-free
305
- doc 2 5421 glm-5.3-flash-free
306
- schema 1 3390 deepseek-v4.1-flash-free
307
- sweep 3 4221 nemotron-3.5-super-vl-free
308
- longform 2 9382 longcat-2.5-preview-free
456
+ lane reqs avg ms avg ctx model
457
+ ----------------------------------------------------------------------
458
+ plan 0 - - deepseek-v4.1-flash-free never used
459
+ build 87 9935 - minimax-m3.1-flash-free, deepseek-v4.1-flash-free
460
+ reviewer 0 - - glm-5.3-flash-free never used
461
+ ...
462
+ 9 lane(s) were never used cut candidates, not defects
463
+ one request is one assistant message; avg ms covers the requests that finished
309
464
  ```
310
465
 
311
466
  A lane that is never used is a description the model reads on every turn and
312
467
  a free model doing nothing. Cut it from the profile's `roles` and the lane
313
468
  leaves the config with it.
314
469
 
470
+ The `build` row is not comparable with the others: it includes the turns
471
+ that did the delegating, so it counts the work of the other nine lanes. A
472
+ one-line `build` reply on its own is about a third of that number, and
473
+ rather more on a cold start. [`docs/ROUTING.md`](docs/ROUTING.md) has the
474
+ per-profile tables and what each row measures.
475
+
315
476
  One request is one assistant message, which is one round trip to the provider:
316
477
  a lane that makes three tool calls makes three requests. The counters come
317
478
  from `.space/telemetry.jsonl`, written by the plugin, never sent anywhere, and
@@ -326,12 +487,12 @@ From one real install:
326
487
 
327
488
  ```
328
489
  CONTEXT PER REQUEST
329
- p50 100,942 p90 256,940 p99 481,687 max 547,986
330
- fresh input p50 528
490
+ p50 99,648 p90 246,536 p99 482,779 max 547,986
491
+ fresh input p50 534
331
492
  STORED MESSAGE BYTES
332
- tool 116.58 MB 77.9%
333
- reasoning 23.10 MB 15.4%
334
- text 3.12 MB 2.1%
493
+ tool 114.78 MB 77.9%
494
+ reasoning 22.94 MB 15.6%
495
+ text 3.04 MB 2.1%
335
496
  READ PRESSURE
336
497
  read calls 2,966
337
498
  grep/glob/list 249
@@ -340,15 +501,15 @@ READ PRESSURE
340
501
  cut by the 200-line cap 441
341
502
  re-reads with an offset 423
342
503
  PERMISSION PRESSURE
343
- prompt with kit allowlist 1,002 (18.5%)
504
+ prompt with kit allowlist 1,002 (19.1%)
344
505
  compaction events 0 (never fired)
345
506
  ```
346
507
 
347
508
  Four numbers in that report changed how this kit is configured, and two of them
348
509
  reversed an earlier decision:
349
510
 
350
- **Tool output is 78% of stored context**, and sessions re-read ~101k tokens per
351
- request to add ~528 new ones. That is why pruning here is deterministic and
511
+ **Tool output is 78% of stored context**, and sessions re-read ~100k tokens per
512
+ request to add ~534 new ones. That is why pruning here is deterministic and
352
513
  unconditional.
353
514
 
354
515
  **Read pressure is 11.9 whole-file reads per search.** Every production harness
@@ -364,6 +525,47 @@ reads exceed 51,200 bytes, so that ceiling is not what limits context.
364
525
 
365
526
  Your numbers stay on your machine; only the script is in the repo.
366
527
 
528
+ `npm run measure` is an **observation** about one install: it compares the
529
+ work before the kit landed with the work after, and the work is never the
530
+ same. It says so when the two windows are not comparable, and the gate uses
531
+ the number that is.
532
+
533
+ `npm run measure:prune` is the attributable one. opencode stores the message
534
+ parts unpruned — the pruner rewrites the outgoing request, not the history —
535
+ so the stored parts of a real session are exactly what the pruner was given,
536
+ and replaying the real pruner over them measures exactly what it removed, on
537
+ the same messages with and without. On one install, on 2026-10-20:
538
+ **80.5% of the context across 60 real sessions**. The number moves with the
539
+ sessions, because it is measured over the ones this machine happened to
540
+ have - so run the command for the number on yours.
541
+
542
+ ## Memory
543
+
544
+ The kit does not implement memory. A local vector store with embeddings, a
545
+ web UI, auto-capture and a user profile is a large thing to build badly, and
546
+ [`opencode-mem`](https://github.com/tickernelz/opencode-mem) already is one.
547
+
548
+ What the kit does is make it one flag away and keep the config honest about
549
+ it:
550
+
551
+ ```bash
552
+ oc-harness use fleet-safe --memory # adds opencode-mem to the plugin list
553
+ oc-harness use fleet-safe --memory=opencode-mem@2.32.0 # a pinned version
554
+ ```
555
+
556
+ Your own plugins are never touched — the entry is **added**, so a config that
557
+ already has it does not grow a second copy, and a config that already has
558
+ other plugins keeps them.
559
+
560
+ The memory plugin keeps its own configuration in
561
+ `~/.config/opencode/opencode-mem.jsonc`. It stores and searches without one,
562
+ but **auto-capture needs a provider** to summarise a session into a memory, so
563
+ give it one:
564
+
565
+ ```jsonc
566
+ { "opencodeProvider": "openai", "opencodeModel": "inherit" }
567
+ ```
568
+
367
569
  ## The plugins
368
570
 
369
571
  | module | hook | job |
@@ -371,7 +573,21 @@ Your numbers stay on your machine; only the script is in the repo.
371
573
  | `guard.ts` | `tool.execute.before` | Blocks destructive commands in code. Fail-closed. |
372
574
  | `prune.ts` | `experimental.chat.messages.transform` | Replaces old tool output with a one-line stub. |
373
575
  | `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`, `chat.message`, `tool.execute.after` | Local JSONL, gitignored, never a network call. |
576
+ | `telemetry.ts` | `event`, `tool.execute.after` | Local JSONL, gitignored, never a network call. |
577
+
578
+ Both `.space/telemetry.jsonl` and `.space/prune.jsonl` are written in the
579
+ **project**, not wherever opencode was started from. The prune log used to
580
+ land in the process's working directory, which is often your home directory.
581
+
582
+ **The utility route.** `small_model` is what opencode uses for its short
583
+ auxiliary calls — a title, a one-line summary, a question rewritten. It used
584
+ to be the session model, so every one of those calls paid the session model's
585
+ *reasoning*, and reasoning, not output, is what a short call costs. Measured
586
+ on this tier, a title costs **49 reasoning tokens on DeepSeek and 332 on
587
+ Nemotron 3.5 Super VL**, and DeepSeek answers in 7.2 s against 22.7 s for
588
+ GLM. A fleet now names a cheaper route; a single-model profile keeps its one
589
+ route, because "one route, and it is the one you asked for" is a promise those
590
+ profiles make. The generated file says which route it chose.
375
591
 
376
592
  `guard.ts` matches against the command a shell would actually run, not the text
377
593
  it was handed. It splits on shell separators, honours quotes so that a `;`
@@ -383,11 +599,19 @@ not guard-blocked; it falls through to the `*` ask permission so opencode
383
599
  prompts for human approval before publishing.
384
600
 
385
601
  Writes are scoped to the project directory, with the OS temp directory as the
386
- one exception GLOBAL.md allows.
602
+ one exception GLOBAL.md allows. That covers every tool that writes: `write`
603
+ and `edit` by their path, `apply_patch` by the paths named in the patch, and
604
+ a bash command by its **working directory** — a command run from `/etc` is
605
+ writing outside the project, whatever the command is.
606
+
607
+ A write tool whose arguments name no file is blocked rather than allowed
608
+ through: a guard that cannot see what a write touches cannot scope it. And
609
+ if the guard itself cannot decide, it blocks — a guard that quietly passes
610
+ when broken is worse than none.
387
611
 
388
612
  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, since
390
- stale reasoning is worse than none.
613
+ verbatim, and never touches the in-flight turn. Old reasoning is dropped,
614
+ since stale reasoning is worse than none.
391
615
 
392
616
  `router.ts` fails over **per lane**. A fleet has ten routes, so one spent
393
617
  route must not take the other nine down with it: the lane that used the dead
@@ -496,17 +720,37 @@ Writes outside the project still ask, via `external_directory`.
496
720
 
497
721
  | lane | mode | job | effort |
498
722
  |---|---|---|---|
499
- | `build` | primary | writes and verifies code | middle |
500
- | `plan` | primary | turns a request into an executable plan | highest |
501
- | `reviewer` | subagent | two passes, never edits | highest |
502
- | `locate` | subagent | finds code, reports `file:line`, never designs | lowest |
503
- | `verify` | subagent | runs checks, never fixes them | lowest |
723
+ | `build` | primary | writes and verifies code | `max` |
724
+ | `plan` | primary | turns a request into an executable plan | `max` |
725
+ | `reviewer` | subagent | two passes, never edits | `max` |
726
+ | `locate` | subagent | finds code, reports `file:line`, never designs | `low` |
727
+ | `verify` | subagent | runs checks, never fixes them | `low` |
504
728
  | `vision` | subagent | reads an image or a screenshot | model's |
505
729
  | `doc` | subagent | reads a long document or a spec | model's |
506
- | `schema` | subagent | produces JSON a program can parse | highest |
507
- | `sweep` | subagent | bulk mechanical sweep, no judgement | lowest |
730
+ | `schema` | subagent | produces JSON a program can parse | `max` |
731
+ | `sweep` | subagent | bulk mechanical sweep, no judgement | `low` / `none` |
508
732
  | `longform` | subagent | very long context, or several answers | model's |
509
733
 
734
+ The effort column is the request in the profile, resolved against the levels
735
+ of the model the lane actually runs on — so `max` is `max` where the model
736
+ publishes it and the nearest level that does exist where it does not, and
737
+ `none` is no variant at all on a model that cannot turn reasoning off. Run
738
+ `oc-harness show <profile>` for the resolved values and the note explaining
739
+ any difference.
740
+
741
+ The tier is worth protecting: measured on one install, DeepSeek spends **320
742
+ reasoning tokens a message at `max` and 19 at `low`**, so the lever is a
743
+ factor of 17. A lane that asks for a tier and lands on a model that publishes
744
+ none runs at that model's natural rate instead, and the generated config says
745
+ so in the form `locate=unset(asked low)`.
746
+
747
+ Three lanes in the unrestricted fleet cannot get their tier, and the
748
+ measurement says they are the three cheapest reasoners on the tier — 116, 59
749
+ and 18 reasoning tokens a message, against 289 for `build` and 198 for
750
+ `plan`, which keep their tier on purpose. The one lane that was genuinely
751
+ expensive was `sweep`, running on a model that reasons 420 tokens a message
752
+ for what is supposed to be a mechanical grep. It does not any more.
753
+
510
754
  `locate` is deliberately *not* called `scout`: OpenCode ships a built-in `scout`
511
755
  subagent for dependency and external-docs research, and shipping our own file
512
756
  under that name shadowed it with `bash: deny` so it could not do the one thing
@@ -516,6 +760,11 @@ Each lane carries a `steps` cap. It is a loop breaker, not a quality knob: on
516
760
  exhaustion OpenCode forces a text-only response, so an agent that has stopped
517
761
  making progress reports rather than looping until the context is gone.
518
762
 
763
+ Each lane also gets its own `permission.task` rule — `deny` for everything
764
+ except the lanes it may hand work to. That is the mechanism behind "hand the
765
+ job to the lane that has it": a lane that is not allowed to spawn anything
766
+ cannot quietly become a general-purpose agent on the most expensive tier.
767
+
519
768
  ## The skill loop
520
769
 
521
770
  The lanes above are the *who*. The skills are the *order*, and they load by
@@ -537,6 +786,44 @@ the conversation instead of the requirement.
537
786
  Publishing is never a loop step. `space-finish` prepares the change; a human
538
787
  decides it ships.
539
788
 
789
+ ## What it costs, measured
790
+
791
+ The kit's own A/B - the same task with and without it, ten reps per arm, 120
792
+ agent runs, both arms on the same model, on this repository at a pinned commit
793
+ so anyone can repeat it:
794
+
795
+ | metric (median) | kit on | kit off |
796
+ |---|---|---|
797
+ | **correct** | **54/60** | **55/60** |
798
+ | tool calls | 12.5 | 8 |
799
+ | input tokens | 56,205 | 10,540 |
800
+ | final context | 39,928 | 12,718 |
801
+ | wall seconds | 43 | 21 |
802
+ | paths written outside the target | 0 | 0 |
803
+
804
+ **On a task that fits in one read, the kit is a cost.** It spends over five
805
+ times the input tokens and takes twice as long, and the answers are a tie -
806
+ 54 against 55, and a second run of the same 120 runs gave 55 against 54.
807
+ The two edit tasks are a tie at 10/10, and the guard stopped no writes in
808
+ either arm.
809
+
810
+ The earlier version of this table said 52/60 against 59/59 - the kit looked
811
+ clearly worse - and that was the grader's fault: it could not read a
812
+ citation written as `**File:** x` on one line and `**Line:** 105` on the
813
+ next, which is the shape some of the kit's answers took. Re-scoring the
814
+ same 120 runs with the fixed grader gives 34/40 against 30/40 on the lookup
815
+ tasks - the kit ahead, where the old grader had it behind. The full account is in [eval/RESULTS.md](eval/RESULTS.md).
816
+
817
+ What the kit buys is in the two places that suite cannot see: **long
818
+ sessions**, where the pruner removes 65-80% of the context depending on the
819
+ sessions it is measured over, and **process** -
820
+ a reviewer that reviews and does not edit, a verify that runs the checks and
821
+ does not fix them, a publish decision that belongs to a human.
822
+
823
+ Whether it is worth it depends on whether the work is long. A repository of
824
+ one-file lookups should not install this. The full table is in
825
+ [eval/RESULTS.md](eval/RESULTS.md).
826
+
540
827
  ## Known limits
541
828
 
542
829
  Read [docs/KNOWN-LIMITS.md](docs/KNOWN-LIMITS.md) before relying on failover.
@@ -549,7 +836,7 @@ session starts.
549
836
 
550
837
  ```bash
551
838
  npm install
552
- npm run check # everything: build, typecheck, tests, live install, verify
839
+ npm run check # everything offline: build, typecheck, tests
553
840
  ```
554
841
 
555
842
  Or one layer at a time:
@@ -557,6 +844,7 @@ Or one layer at a time:
557
844
  ```bash
558
845
  npm test # model-agnostic assertions, no network, no model calls
559
846
  npm run typecheck
847
+ npm run check:install # the above, plus the live install measurement and verify
560
848
  npm run eval # synthetic: pruning, prompt pressure, effort projection
561
849
  npm run measure # is this install behaving? narrow reads, context
562
850
  npm run attribute # where the context went: fixed overhead vs history
@@ -565,8 +853,8 @@ npm run baseline # your own install, from scratch
565
853
  ```
566
854
 
567
855
  `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 `npm run check`, and
569
- it wants a subject it can hand each run a private copy of:
856
+ from the work. It spawns real agents, so it is not part of any check, and it
857
+ wants a subject it can hand each run a private copy of:
570
858
 
571
859
  ```bash
572
860
  node eval/prepare.mjs --repo ~/work/lablab-harness --ref 8a674f5
@@ -576,7 +864,7 @@ node eval/ab.mjs --rescore <saved stream dir> # re-grade without spawning agen
576
864
 
577
865
  `npm test` and `npm run typecheck` need nothing but the repo, so CI runs those.
578
866
  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 -
867
+ which only exist on a machine that has run OpenCode with the kit installed —
580
868
  `npm run check:online` additionally hits the model catalog over the network.
581
869
 
582
870
  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.