opencode-harness-kit 0.4.2 → 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.
Files changed (73) hide show
  1. package/CHANGELOG.md +544 -19
  2. package/CONTRIBUTING.md +242 -0
  3. package/GLOBAL.md +34 -5
  4. package/MIGRATION.md +151 -0
  5. package/README.md +294 -37
  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 +1265 -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 +71 -0
  27. package/docs/KNOWN-LIMITS.md +45 -21
  28. package/docs/RESEARCH.md +9 -4
  29. package/docs/ROUTING.md +53 -8
  30. package/eval/RESULTS.md +443 -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 +323 -0
  35. package/eval/measure-prune.mjs +339 -0
  36. package/eval/metrics.js +79 -0
  37. package/eval/premises.mjs +200 -0
  38. package/eval/prepare.mjs +233 -0
  39. package/eval/run.mjs +376 -0
  40. package/eval/tasks-self.json +81 -0
  41. package/eval/tasks-ts.json +50 -0
  42. package/eval/tasks.json +83 -0
  43. package/eval/thresholds.mjs +134 -0
  44. package/eval/verify.mjs +287 -0
  45. package/package.json +44 -17
  46. package/profiles/fleet-all.json +6 -1
  47. package/profiles/fleet-safe.json +7 -3
  48. package/profiles/schema.json +182 -51
  49. package/prompts/build.md +4 -2
  50. package/skills/space-build/SKILL.md +4 -2
  51. package/skills/space-finish/SKILL.md +6 -2
  52. package/skills/space-plan/reference/task-template.md +13 -5
  53. package/src/config.js +182 -32
  54. package/src/denylist.js +95 -0
  55. package/src/driver/run.mjs +81 -16
  56. package/src/effort.js +15 -4
  57. package/src/guard.ts +165 -13
  58. package/src/health.js +80 -0
  59. package/src/index.ts +15 -4
  60. package/src/lane-usage.js +72 -18
  61. package/src/plugin-type-check.ts +38 -0
  62. package/src/plugin.ts +39 -0
  63. package/src/profile.js +225 -26
  64. package/src/project.js +47 -0
  65. package/src/prune.ts +110 -31
  66. package/src/router.ts +32 -63
  67. package/src/rules.js +99 -7
  68. package/src/safe.ts +27 -0
  69. package/src/sqlite.js +144 -0
  70. package/src/tasks.js +112 -29
  71. package/src/telemetry-store.ts +62 -0
  72. package/src/telemetry.ts +60 -57
  73. package/src/trust.js +70 -9
@@ -96,8 +96,8 @@ a client cannot currently tell a short quota window from a permanent block.
96
96
 
97
97
  ## A copied plugin loads and does nothing
98
98
 
99
- The bundle finds `profiles/`, `prompts/`, `retention.yml` and `lanes.json` by
100
- walking up from its own file. A symlink resolves to the real path, so a
99
+ The bundle finds `profiles/` and `lanes.json` by walking up from its own file,
100
+ and `retention.yml` next to them. A symlink resolves to the real path, so a
101
101
  symlinked plugin finds the kit's data. A **copy** does not: it looks beside
102
102
  itself, finds nothing, and every profile lookup fails.
103
103
 
@@ -284,23 +284,47 @@ unhealthy and failing over at the next start is the honest behaviour.
284
284
  ## Free models get withdrawn quickly
285
285
 
286
286
  From one install's 20-day history, free model slots turned over in days:
287
-
288
- | model | observed lifetime |
289
- |---|---|
290
- | `mimo-v2.5-free` | 1 day |
291
- | `big-pickle` | 1 day |
292
- | `nemotron-3-ultra-free` | 3 days |
293
- | `muse-spark-1.3-contributor-free` | still running at 20 days |
294
-
295
- The tier turned over again since: four of this kit's five original profiles
296
- (`space-bunny-free`, `x-preview-f-free`, `deepseek-v4-flash-free`,
297
- `muse-spark-1.3-contributor-free`) were all withdrawn within a month of
298
- shipping, and the default profile pointed at a model that no longer existed.
299
-
300
- Zen states free models are available "for a limited time" with no date. Treat
301
- profiles as perishable: run `oc-harness doctor`, which re-checks each profile
302
- against live metadata and reports drift, and which also reports a lane whose
303
- whole chain has left the catalogue.
287
+ `mimo-v2.5-free` lasted a day, `big-pickle` a day, `nemotron-3-ultra-free`
288
+ three days. Zen states free models are available "for a limited time" with no
289
+ date. Treat profiles as perishable.
290
+
291
+ The README's [Free models are perishable](../../README.md#free-models-are-perishable-so-check-them)
292
+ section has the full list of withdrawn models, the four commands that check
293
+ for drift, and what `refresh` will and will not rewrite. The short version:
294
+ run `oc-harness doctor` before every release and whenever a lane starts
295
+ failing with a 429 or a withdrawal.
296
+
297
+ The tier turned over again since. Every one of this kit's original
298
+ profiles has been withdrawn — `space-bunny-free`, `x-preview-f-free`,
299
+ `deepseek-v4-flash-free`, `muse-spark-1.3-contributor-free` and
300
+ `mimo-v2.5-free` — and the default profile pointed at a model that no longer
301
+ existed.
302
+
303
+ ## The install-wide measurement is an observation, and there is a controlled one
304
+
305
+ `npm run measure` compares the work before the kit landed with the work
306
+ after. It reported the context per request going **up** by 16% on one install,
307
+ with twice as many reads and twice as many messages in the second window.
308
+
309
+ That is a number about the jobs, not about the kit. The two windows are never
310
+ the same work, and "everything since the install" is a window that grows
311
+ without bound, so the comparison is a different one every time the script
312
+ runs.
313
+
314
+ `npm run measure` now says so: the windows are fixed and equal around the
315
+ split, the context drop is skipped with a note when the two sides are not
316
+ comparable, and the gate uses the number that IS comparable.
317
+
318
+ `eval/measure-prune.mjs` is that number. opencode stores the message parts
319
+ unpruned - the pruner rewrites the outgoing request, not the history - so the
320
+ stored parts of a real session are exactly what the pruner was given, and
321
+ replaying the real pruner over them measures exactly what it removed. On one
322
+ install: **80.5% of the context
323
+ across 60 real sessions**, 6,608 parts stubbed - the number
324
+ `npm run measure:prune` prints.
325
+
326
+ The controlled comparison of the two arms is `eval/ab.mjs`, and its result is
327
+ in `eval/RESULTS.md`.
304
328
 
305
329
  ## A model-driven compressor is not assumed to work
306
330
 
@@ -322,7 +346,7 @@ truncation directory, so re-reading is possible. But "re-run the command to see
322
346
  it again" is more expensive than "scroll up". The stub is one line precisely so
323
347
  an agent can decide whether it is worth it, and the stub says so.
324
348
 
325
- A `savedBytes` of 0 in `prune.jsonl` is not a second, redundant pass. It means
349
+ A `savedChars` of 0 in `prune.jsonl` is not a second, redundant pass. It means
326
350
  `bytesAfter` did not come out below `bytesBefore` for that turn, which happens
327
351
  when a stub is no shorter than the output it replaced (the stub's own label
328
352
  costs more than a short body), or when a `reasoning` part was under the 200
@@ -346,7 +370,7 @@ compressor, or a session UI. Those exist and are good:
346
370
  |---|---|
347
371
  | model-driven compression | `@tarquinen/opencode-dcp` |
348
372
  | cross-harness context proxy | `sleev.ai` |
349
- | persistent memory | `opencode-supermemory` |
373
+ | persistent memory | [`opencode-mem`](https://github.com/tickernelz/opencode-mem), which `oc-harness use --memory` turns on |
350
374
  | git worktrees | `kdcokenny/opencode-worktree` |
351
375
  | memory in git markdown | `code-yeongyu/oh-my-opencode` (now `omo`) |
352
376
  | agent DAGs | `spoons-and-mirrors/subtask2` |
package/docs/RESEARCH.md CHANGED
@@ -47,13 +47,18 @@ ceiling that a normal session never reaches.
47
47
 
48
48
  5,239 historical `bash` commands were replayed against the allowlist shapes:
49
49
 
50
- | allowlist | prompts | share |
50
+ | allowlist | prompts | share of 5,239 |
51
51
  |---|---|---|
52
52
  | the narrow one in use | 4,994 | 95.3% |
53
53
  | this kit's, first version | 1,204 | 23.0% |
54
- | this kit's, current | 1,002 | 18.5% |
54
+ | this kit's, current | 1,002 | 19.1% |
55
55
 
56
- The remaining 18.5% is the deliberately gated set, ranked by how often it
56
+ The three rows share the denominator, and the arithmetic is theirs to be
57
+ checked: 4,994/5,239 is 95.3%, 1,204/5,239 is 23.0%, and 1,002/5,239 is
58
+ 19.1%. The last figure was quoted as 18.5% in two places, which is a
59
+ different number for the same count.
60
+
61
+ The remaining 19.1% is the deliberately gated set, ranked by how often it
57
62
  actually came up:
58
63
 
59
64
  ```
@@ -111,7 +116,7 @@ is what actually bounds context.
111
116
  `bin/baseline.mjs` now prints all five numbers so this is re-measurable instead
112
117
  of being re-argued.
113
118
 
114
- **"Compaction never ran, therefore nothing is wrong."** Zero events across 156
119
+ **"Compaction never ran, therefore nothing is wrong."** Zero events across 153
115
120
  sessions was read as "the setting is fine" rather than "nothing is being
116
121
  bounded". Both readings were half right and the compound conclusion was wrong:
117
122
  the configured `tail_turns` was inert *and* there was no bound on growth.
package/docs/ROUTING.md CHANGED
@@ -23,7 +23,7 @@ models. A profile says which lanes it enables and which model serves each one.
23
23
  | `vision` | read an image or a screenshot | mimo-v2.6-flash-free | minimax-m3.1-flash-free |
24
24
  | `doc` | read a long document or a spec | glm-5.3-flash-free | glm-5.3-flash-free |
25
25
  | `schema` | produce JSON or a JSON schema | deepseek-v4.1-flash-free | deepseek-v4.1-flash-free |
26
- | `sweep` | sweep a lot of code mechanically | nemotron-3.5-super-vl-free | longcat-2.5-preview-free |
26
+ | `sweep` | sweep a lot of code mechanically | nemotron-3.5-super-vl-free | deepseek-v4.1-flash-free |
27
27
  | `longform` | read a megabyte, or write alternatives | longcat-2.5-preview-free | longcat-2.5-preview-free |
28
28
 
29
29
  `fleet-all` gives every lane the model built for it: ten jobs, ten models,
@@ -35,6 +35,17 @@ ceiling, and it is worth paying only when the model that takes over is the
35
35
  best one available *for that job*, which is why the two fleets order their
36
36
  chains differently (see below).
37
37
 
38
+ The `sweep` lane is the one that was worth changing rather than accepting.
39
+ A bulk mechanical search is the one job where reasoning is pure waste, and
40
+ the model it ran on reasoned 420 tokens a message at 16.6 seconds - the
41
+ most expensive way to ask a model to grep. It runs on DeepSeek now, which is
42
+ zero-retention, publishes a `low` level, and measures 19 reasoning tokens at
43
+ 3.4 seconds.
44
+
45
+ Under that ceiling every lane that asks for a tier GETS one. The safe fleet
46
+ has no lane with a dropped tier request; the unrestricted fleet has three, and
47
+ the measurement says they are the three cheapest reasoners on the tier.
48
+
38
49
  ## Measured, per lane
39
50
 
40
51
  Every number here is a live call, not a catalog reading. A **request** is one
@@ -83,6 +94,27 @@ wait. Reviewing barely pays at all: 4716 ms on Nemotron 3 Ultra against 5757
83
94
  ms on GLM. Locating and verifying get *faster*, because DeepSeek publishes a
84
95
  `low` effort level and Big Pickle publishes none at all.
85
96
 
97
+ **What a tier is worth, measured on this install rather than assumed.** The
98
+ effort level is the cheapest lever there is: DeepSeek measures 320 reasoning
99
+ tokens a message at `max` and 19 at `low`, a factor of 17, and a lane that
100
+ asks for a tier and lands on a model that publishes none runs at that model's
101
+ NATURAL rate instead. So the tier request is worth protecting.
102
+
103
+ The three lanes in `fleet-all` whose model cannot serve the tier - `reviewer`
104
+ on Nemotron 3 Ultra, `locate` on Nemotron 3.5 Lightning, `verify` on Big
105
+ Pickle - are the fleet's CHEAPEST reasoners: 116, 59 and 18 reasoning tokens a
106
+ message, against 289 for `build` and 198 for `plan`, which keep their tier on
107
+ purpose. The tier is the lever, and it is not where the tokens are. The
108
+ config header says so where it happens, in the form `locate=unset(asked low)`.
109
+
110
+ One lane was not cheap, and it was fixed. `sweep` is a bulk mechanical
111
+ search, so reasoning on it is pure waste, and it ran on LongCat: 420 reasoning
112
+ tokens a message at 16.6 s, the most expensive way to ask a model to grep. No
113
+ zero-retention model publishes a `none` level, so the lane could not even turn
114
+ reasoning off when it asked to. It runs on DeepSeek now, which is
115
+ zero-retention, publishes a `low` level, and measures 19 reasoning tokens at
116
+ 3.4 s.
117
+
86
118
  And one lane gets faster rather than slower. MiMo reads an image in 19.2 s;
87
119
  MiniMax reads the same image in 7.7 s. The unrestricted fleet put `vision` on
88
120
  the slower model because MiMo is the tier's dedicated image reader, and under
@@ -147,6 +179,11 @@ mis-served by that:
147
179
  - `locate` asked for `low` effort and landed on a model that publishes no
148
180
  effort levels at all, so the request was dropped on the floor. It now lands
149
181
  on DeepSeek, which publishes `low`, and measures 3612 ms.
182
+ - `sweep` is a bulk mechanical search, so reasoning on it is pure waste. It
183
+ ran on LongCat at 420 reasoning tokens a message and 16.6 s - and no
184
+ zero-retention model publishes a `none` level, so the lane could not turn
185
+ reasoning off even when it asked to. It runs on DeepSeek now: 19 reasoning
186
+ tokens at 3.4 s.
150
187
  - `vision` landed on the same model, at 9-13 s, when MiniMax reads the same
151
188
  image in 7.7 s.
152
189
 
@@ -170,14 +207,22 @@ base profile would reveal.
170
207
 
171
208
  ### What the safe fleet gives up
172
209
 
173
- `sweep` is the one lane that loses its reason to exist under the ceiling.
210
+ `sweep` was the one lane that lost its reason to exist under the ceiling.
174
211
  Super VL is the only model on the tier that publishes a `none` effort level,
175
- so "reasoning off" is a job of its own; and Super VL is a may-train provider,
176
- so the ceiling excludes it. Under `fleet-safe` the lane falls back to
177
- LongCat, which publishes no effort levels at all, and it is a second `locate`
178
- with a 1M context window. The lane is kept, because `oc-harness lanes` should
179
- decide that from measurements rather than from a guess - but the docs say out
180
- loud that the capability is gone.
212
+ so "reasoning off" is a job of its own; and Super VL is a may-train
213
+ provider, so the ceiling excludes it. Under `fleet-safe` the lane ran on
214
+ LongCat, which publishes no effort levels at all, and it was a second
215
+ `locate` with a 1M context window.
216
+
217
+ It runs on DeepSeek now. Not LongCat: LongCat measured 420 reasoning tokens
218
+ a message at 16.6 seconds, which is the most expensive way to ask a model to
219
+ grep, and DeepSeek publishes a `low` level and measures 19 reasoning tokens
220
+ at 3.4. The lane is still not the unrestricted fleet's lane, and the
221
+ reasoning-off capability is still out of reach under a zero-retention
222
+ ceiling - but the cost of the substitute is no longer the whole story.
223
+
224
+ The lane is kept, because `oc-harness lanes` should decide that from
225
+ measurements rather than from a guess.
181
226
 
182
227
  ## Measuring, and cutting
183
228
 
@@ -0,0 +1,443 @@
1
+ # Measured results
2
+
3
+ Numbers the kit actually produced on a real install, and how far to trust them.
4
+ Regenerate with:
5
+
6
+ npm run measure # human-readable table
7
+ npm run measure -- --json # machine-readable
8
+ npm run measure:prune # the pruner, on real sessions
9
+ npm run measure:check # the gate
10
+ npm run attribute # where the context actually went
11
+
12
+ The source is the install database, opened read-only. Nothing here is estimated
13
+ and nothing is estimated from a synthetic transcript: `eval/run.mjs` covers the
14
+ pruner and the allowlist, and cannot say anything about how the agent reads.
15
+
16
+ Everything in this file comes from the commands above. The A/B section at the
17
+ bottom runs the same task with and without the kit, ten times each, on both
18
+ arms at once - 120 agent runs, which is enough to be worth reading and not
19
+ enough to be worth trusting.
20
+
21
+ ## Two measurements, and the difference between them
22
+
23
+ **`npm run measure` is an OBSERVATION about one install.** It compares the work
24
+ before the kit landed with the work after, and the work is never the same. It
25
+ reported the context per request going UP by 16% on one install, with twice as
26
+ many reads and twice as many messages in the second window - a number about
27
+ the jobs, not about the kit. Its windows are now fixed and equal around the
28
+ split, and the report says how long they were; the context drop is skipped
29
+ with a note when the two sides did wildly different amounts of work, because a
30
+ gate that fails for reasons nobody can act on is a gate people switch off.
31
+
32
+ **`npm run measure:prune` is ATTRIBUTABLE.** opencode stores the message
33
+ parts unpruned - the pruner rewrites the outgoing request, not the history -
34
+ so the stored parts of a real session are exactly what the pruner was given.
35
+ Replaying the real pruner over them measures what it removed, on the same
36
+ messages, with and without.
37
+
38
+ npm run measure:prune
39
+
40
+ | | |
41
+ |---|---|
42
+ | sessions | 60 (last 30d, at least 6 assistant messages each) |
43
+ | context before | 15,130,845 characters |
44
+ | context after | 2,953,140 characters |
45
+ | **removed** | **80.5%** |
46
+ | parts stubbed | 6,608 |
47
+
48
+ **`eval/ab.mjs` is the CONTROLLED comparison** - the same task with and
49
+ without the kit, graded on the file after the run. It is at the bottom.
50
+
51
+ ## Method
52
+
53
+ Three edges divide the database into two windows:
54
+
55
+ - **split** - when the kit was installed, taken from the mtime of
56
+ `~/.config/opencode/agents`, which `oc-harness install` creates and nothing
57
+ else does.
58
+ - **before** / **after** - the outer bounds of the two windows.
59
+
60
+ `npm run measure` defaults BOTH edges to seven days around the split, so the
61
+ two windows are the same length. `--before` takes an ISO timestamp, so
62
+ `--before 2026-09-17T15:38:46.850Z` is how the original baseline in
63
+ `README.md` was reproduced. `--days` changes the window length, and `--since`
64
+ puts the unbounded "everything since the install" window back.
65
+
66
+ A read counts as **narrow** when the caller bounded it with `offset` or
67
+ `limit`. `README.md` counted `offset` alone; that is the 423 figure below, and
68
+ this script's 501 is the same reads plus the ones bounded only by `limit`.
69
+
70
+ **Results 1 and 2 are a snapshot, not a live reading.** They were taken from
71
+ this install on 2026-10-20, and they describe the windows of that date. The
72
+ commands above reproduce them against the same database; against a later one
73
+ they show later windows, because the windows are anchored on the install, not
74
+ on the day of writing. The pruner number in the table near the top is the
75
+ one that moves with the data, because it replays the real pruner over real
76
+ sessions and holds everything else still.
77
+
78
+ ## Result 1 - a week before the install against a week after
79
+
80
+ npm run measure
81
+
82
+ | Metric | before (Oct 1 - Oct 8) | after (Oct 8 - Oct 10) | change |
83
+ | --- | --- | --- | --- |
84
+ | read calls | 767 | 280 | -63% |
85
+ | narrow reads | 229 (29.9%) | 184 (**65.7%**) | +35.8 pt |
86
+ | grep/glob calls | 61 | 66 | +8% |
87
+ | reads per search | 12.6 | **4.2** | -67% |
88
+ | assistant messages | 5,616 | 1,562 | |
89
+ | avg context per request | 158,917 | 67,642 | **-57.4%** |
90
+ | avg reasoning tokens | 151 | 63 | -58% |
91
+
92
+ Searching more and reading three times less per search is the intended shape of
93
+ the change. The instruction file is what moved it: it tells the agent to locate
94
+ before reading, and the narrow-read share is the direct trace of that.
95
+
96
+ ## Result 2 - the full history, reproducing the published baseline
97
+
98
+ npm run measure -- --before 2026-09-17T15:38:46.850Z
99
+
100
+ | Metric | before (Sep 17 - Oct 8) | after | change |
101
+ | --- | --- | --- | --- |
102
+ | read calls | 2,966 | 280 | |
103
+ | narrow reads | 501 (16.9%) | 184 (65.7%) | |
104
+ | grep/glob calls | 249 | 66 | |
105
+ | reads per search | 11.9 | 4.2 | -65% |
106
+ | avg context per request | 123,776 | 67,642 | **-45.4%** |
107
+
108
+ This reproduces the `2,966 reads / 249 searches / 11.9` baseline published in
109
+ `README.md` exactly, which is the check that the measurement is reading the same
110
+ things the earlier one did. The before-window here starts at the beginning of
111
+ the database, so it is wider than the default and the before-numbers differ.
112
+
113
+ ## Result 3 - why the context fell, decomposed
114
+
115
+ npm run attribute
116
+
117
+ Result 2 shows context per request down 45%. It does not say which part of the
118
+ context moved. There are two, and they behave independently:
119
+
120
+ - **fixed overhead** - the system prompt and tool definitions, resent on every
121
+ request. Paid on turn one, before any work happens.
122
+ - **history** - what the conversation has accumulated since: tool output, files
123
+ read, the agent's own earlier messages.
124
+
125
+ Per session:
126
+
127
+ | | before | after | delta |
128
+ | --- | --- | --- | --- |
129
+ | sessions | 154 | 48 | |
130
+ | turns per session | 79.1 | 35.2 | |
131
+ | fixed overhead | 8,236 | 8,705 | **+469** |
132
+ | context at end | 68,648 | 18,285 | |
133
+ | history | 60,412 | 9,580 | **-50,832** |
134
+ | history per turn | 763.4 | 272.4 | **-491** |
135
+
136
+ The kit's own cost is visible and it is positive: **+469 tokens on every single
137
+ request**, which is `GLOBAL.md` plus the agent definitions plus the plugin
138
+ prompts. Nothing about the kit made the system prompt shorter.
139
+
140
+ Everything it bought is in the other column. History per session fell by 84%, and
141
+ per turn by 64%. The per-turn column is the one that matters, because sessions
142
+ also got shorter - 79 turns to 35 - and shorter sessions accumulate less history
143
+ for free. Normalising for turns, the kit is still responsible for a 64% drop.
144
+
145
+ That is the mechanism the whole kit rests on, and it is the one thing the
146
+ correlation in Results 1 and 2 could not show. The kit does not shrink what it
147
+ costs; it shrinks what the agent drags along behind it.
148
+
149
+ ## The A/B on TypeScript
150
+
151
+ node eval/ab.mjs --target <dir> --suite eval/tasks-ts.json --reps 2 --sanitize
152
+
153
+ **A pilot, not a result.** The kit is written in TypeScript and the Python suite
154
+ above is Python, which is the wrong language for judging the code. This closes
155
+ the gap just enough to say whether the harness works there.
156
+
157
+ **And not a checkable one.** The subject is a private repository, so nobody
158
+ can repeat this run - which is the same reason the self-suite below exists
159
+ and the same thing this suite cannot be. It is here because the question
160
+ was worth asking once, not because the number is a result. The repeatable
161
+ measurement is the self-suite.
162
+
163
+ Subject: `vendor/gibwork-sentinel` from `gibwork-agent-harness` - 692 lines of
164
+ TypeScript in seven modules, shipped beside compiled JavaScript in `dist/`. Every
165
+ answer occurs in `src/` *and again* in `dist/`, so each task says "in the
166
+ TypeScript source only". An agent that greps and stops at the build output is
167
+ wrong, which is the whole reason this subject is worth having. All three answers
168
+ were checked to occur in exactly one file under `src/` before the tasks were
169
+ written, because two earlier tasks were wrong for failing that check.
170
+
171
+ | Metric (median of 8 runs per arm) | kit on | kit off |
172
+ | --- | --- | --- |
173
+ | wall seconds | 26.1 | **19.4** |
174
+ | tool calls | 5 | 3.5 |
175
+ | narrow reads | 100% | 16.7% |
176
+ | final context | 10,815 | 9,702 |
177
+ | correct | 8 / 8 | 8 / 8 |
178
+ | paths outside target | 0 | 0 |
179
+
180
+ | per task | kit on | kit off |
181
+ | --- | --- | --- |
182
+ | ts-rpc-default | 2/2 | 2/2 |
183
+ | ts-rest-client | 2/2 | 2/2 |
184
+ | ts-pr-template | 2/2 | 2/2 |
185
+ | ts-edit-rpc | 2/2 | 2/2 |
186
+
187
+ Both arms got all four, including the edit. One kit-on answer is worth reading
188
+ for its own sake: it cited `src/scaffold.ts:77` and then said the `dist/` grep
189
+ results were not the answer, because `dist/` is compiled from `src/` and the task
190
+ asked for the source. The trap in this subject is real and the agent saw it.
191
+
192
+ ### The one task that is real work
193
+
194
+ Every task above is a lookup or a one-line edit. `edit-multi-profile` in
195
+ `eval/tasks.json` is the exception and it is the closest thing here to the work
196
+ the kit claims to help with: six stack profiles, each holding a weights
197
+ dictionary, all six have to be edited, and an agent that stops after the first
198
+ two fails.
199
+
200
+ | | kit on | kit off |
201
+ | --- | --- | --- |
202
+ | correct | 2/2 | 1/2 |
203
+ | tools | 8 and 11 | 3 and 9 |
204
+ | narrow reads | 100% | 0% |
205
+
206
+ The failing kit-off run made three tool calls, read nothing narrowly, and left
207
+ the file untouched. **Two runs per arm is not evidence of anything**, and this is
208
+ recorded as the first place the kit came out ahead rather than as a finding. It
209
+ is the one result here worth spending more runs on.
210
+
211
+ Subjects are built with `eval/prepare.mjs`, which pins a git ref through
212
+ `git archive` and does not call a tree ready until it has checked that tree for
213
+ absolute paths pointing at a real checkout.
214
+
215
+ ### What this does not establish
216
+
217
+ - **Eight runs per arm, and every task is a lookup or a one-line edit.** Not the
218
+ kit's own codebase, and nothing here measures a long multi-step refactor.
219
+ - **The speed gap still points the wrong way.** 26.1s against 19.4s, the same
220
+ direction as the five-task Python suite. See the note below.
221
+ - **Wall clock is uncontrolled for load.** Sixteen runs share one model route.
222
+
223
+ ### The one pattern that holds across suites
224
+
225
+ Two suites that contain an edit task show the kit **slower**: Python 25.4s
226
+ against 17.4s, TypeScript 26.1s against 19.4s. The one suite that showed it
227
+ faster - 18.4s against 32.7s - had no edit task and was the smaller of the
228
+ three.
229
+
230
+ That is suggestive and it is the opposite of what the kit is built for. Reading
231
+ narrowly costs an extra tool call per run, 5 against 3.5 here, and on a suite of
232
+ three or four-step lookups that cost does not buy anything back because the bare
233
+ agent was already finding the file. Whether it buys anything back on real work
234
+ is exactly what this suite cannot show, since none of the tasks is real work.
235
+
236
+ **Correctness is a tie in every suite run so far**: 8/8 against 8/8, 8/8 against
237
+ 8/8, and 9/10 against 9/10 before two task ambiguities were fixed. Thirty-eight
238
+ agent runs have not produced a single point of accuracy difference in either
239
+ direction.
240
+
241
+ ## Where the gate thresholds come from
242
+
243
+ `npm run measure:check` fails below 45% narrow reads or a 20% fall in context per
244
+ request. The 45 is derived rather than picked:
245
+
246
+ | day since the install | read calls | narrow |
247
+ | --- | --- | --- |
248
+ | Oct 8 | 29 | 69.0% |
249
+ | Oct 9 | 207 | 60.9% |
250
+ | Oct 10 | 239 | 74.9% |
251
+
252
+ Lowest observed day is 60.9%, and the spread across those three days is 14
253
+ points. A floor one whole spread below the lowest day lands near 45%, so ordinary
254
+ day-to-day movement cannot trip it and anything under it means every day since
255
+ the install was better than the failure. The previous value, 50, sat above that
256
+ margin and would have failed on a day like Oct 9.
257
+
258
+ **Three days is not a confidence interval.** The margin is one observed spread,
259
+ not a standard deviation, and the floor should be re-derived as days accumulate.
260
+ The context floor of 20% is not derived at all: it sits far below the measured
261
+ 45-57% drop, which is the conservative choice, and tightening it would need
262
+ enough days to see how that figure moves.
263
+
264
+ A per-session distribution was also computed and deliberately not used for this.
265
+ Sessions run bimodally - four of them read 0% narrow, mostly three or four read
266
+ calls each - so a per-session floor would land near zero and catch nothing.
267
+
268
+ ## What these numbers do not prove
269
+
270
+ **The two windows are not the same work.** The before-window is dominated by
271
+ building this kit - reading source is what that job was. The after-window is
272
+ ordinary use. Part of the drop is the kit working; an unknown part is simply a
273
+ different mix of tasks. Nothing here separates the two.
274
+
275
+ **The after-window is thin.** 280 reads over roughly two days, and the script
276
+ flags this itself as `thin_sample`. One quiet day moves these numbers more than
277
+ the kit does. Treat the direction as a hint, not a result.
278
+
279
+ **Narrow reads are a proxy.** A bounded read that then has to be re-read with a
280
+ second bounded read is not cheaper than one whole-file read. This counts the
281
+ first read and ignores the follow-up.
282
+
283
+ **Nothing above is an A/B.** The windows in Results 1 and 2 are the same install
284
+ observed over time, and they compare different work. The section below runs one
285
+ fixed task twice - kit on, kit off - which is the only comparison here that can
286
+ say anything about cause.
287
+
288
+ ## The A/B on this repository, 10 reps per arm
289
+
290
+ node eval/prepare.mjs --repo . --ref <sha> --name self
291
+ node eval/ab.mjs --target <scratch>/self --suite eval/tasks-self.json --reps 10
292
+
293
+ **The subject is this repository at a pinned commit**, so the run is
294
+ reproducible: `git clone && git checkout <sha>` and the same command. The
295
+ previous suite ran against a private repository, which meant a result nobody
296
+ else could check.
297
+
298
+ Six tasks, ten reps per arm, 120 agent runs: four lookups and two edits. The
299
+ lookups are graded on the `file:line` the agent cited, read out of the
300
+ snapshot; the edits are graded on the file after the run.
301
+
302
+ **Both arms ran on the same model**, `opencode/minimax-m3.1-flash-free`, and
303
+ the report prints it. The kit-off arm used to run with an empty config, so
304
+ opencode picked its own default: not the model the kit-on arm was on, and
305
+ not a model that answered - four kit-off runs hit the 600-second ceiling
306
+ with no output at all, and a timed-out run has zero tools and zero context,
307
+ so it entered the medians as a fast, empty, wrong answer.
308
+
309
+ | metric (median) | kit on | kit off |
310
+ | --- | --- | --- |
311
+ | **correct** | **54/60** | **55/60** |
312
+ | tool calls | 12.5 | 8 |
313
+ | read calls | 3 | 2 |
314
+ | narrow reads | 66.7% | 66.7% |
315
+ | grep/glob calls | 3 | 2 |
316
+ | steps | 9 | 6 |
317
+ | input tokens | 56,205 | 10,540 |
318
+ | final context | 39,928 | 12,718 |
319
+ | wall seconds | 43 | 21 |
320
+ | paths written outside the target | 0 | 0 |
321
+
322
+ | per task | kit on | kit off |
323
+ | --- | --- | --- |
324
+ | `read-control` | 10/10 | 10/10 |
325
+ | `read-multi-denylist` | 8/10 | 8/10 |
326
+ | `read-multi-gate` | 6/10 | 7/10 |
327
+ | `read-two-files` | 10/10 | 10/10 |
328
+ | `edit-rename` | 10/10 | 10/10 |
329
+ | `edit-multi` | 10/10 | 10/10 |
330
+
331
+ ### What this says
332
+
333
+ **On correctness, the kit is a tie.** 54 against 55, on sixty runs a side.
334
+ That is inside the noise of a single task flipping, and it is the opposite
335
+ of what the previous version of this file said.
336
+
337
+ A second run of the same 120 runs - same subject, same suite, same model,
338
+ a different day of model traffic - gave **55/60 against 54/60**, the kit
339
+ ahead by one. Two independent measurements put the tie on either side of
340
+ zero, which is what a tie looks like from both ends.
341
+
342
+ **On everything else, the kit is a cost.** It spends **5.3x the input
343
+ tokens** and takes **twice as long**, and it makes **more** tool calls.
344
+ Those are the same numbers as before, and they are the honest ones. The
345
+ narrow-read share is the same in both arms - 66.7% - so the kit's rules do
346
+ not make the agent read more carefully; they make it read more, and the
347
+ context that follows is the price.
348
+
349
+ **The previous conclusion was the grader's fault, and it is worth writing
350
+ down.** The earlier run said 52/60 against 59/59 - the kit looked clearly
351
+ worse - and the reason was that the grader could not read a citation
352
+ written as two markdown fields:
353
+
354
+ **File:** `src/profile.js`
355
+ **Line:** 105
356
+
357
+ The old pattern stopped at the newline, so the file and the number were
358
+ never paired, the citation was lost, and the answer was scored wrong. The
359
+ kit's own instructions ask the agent to name the file and the line, and
360
+ some of the answers took the two-field markdown shape - the shape the
361
+ grader could not read. The kit-off arm had no such instructions, mostly
362
+ answered in prose, and was graded by a pattern that handled prose.
363
+
364
+ Re-scoring the SAME run with the fixed grader - the same 120 agent runs,
365
+ the same answers, only the grader changed - gives **34/40 against 30/40** on
366
+ the four lookup tasks, the kit ahead. A harness that measures the harness
367
+ is worse than one that measures nothing.
368
+
369
+ ### What this does not say
370
+
371
+ It does not say the kit is not worth it. It says the kit's value is **not in
372
+ short lookups**, and this suite only contains short lookups and two small
373
+ edits. The value it does have is in two places this suite cannot see:
374
+
375
+ - **Long sessions.** The pruner removes **80.5% of the context** across real
376
+ sessions (`npm run measure:prune`), and a task that fits in one read never
377
+ gets near the point where pruning matters.
378
+ - **Process.** A `reviewer` that reviews and does not edit, a `verify` that
379
+ runs the checks and does not fix them, a `space-finish` that hands the
380
+ publish decision to a human. Those are behaviours, not accuracy, and a
381
+ grader that counts correct answers cannot see them.
382
+
383
+ The honest position is a trade, and it is now a trade with a number on it:
384
+ **the kit costs about five times the input tokens on a small task, gets the
385
+ answer right about as often, and buys process discipline and an 80% smaller
386
+ context on a long one.** Whether that is worth it depends on whether the
387
+ work is long. A repo of one-file lookups should not install this.
388
+
389
+ A fleet that routes each job to the model built for it is the other half of
390
+ what the kit offers, and this suite cannot see that half: it runs one model
391
+ on both sides, deliberately, so that the only variable is the kit. The
392
+ routing is measured separately, in `docs/ROUTING.md`.
393
+
394
+ ## The gate
395
+
396
+ `npm run measure:check` fails when either the narrow-read share drops below 45%
397
+ or average context per request stops falling by at least 20%. Both hold today
398
+ with room to spare (65.7% and 45.4%). The thresholds are set well below the
399
+ current values on purpose: the point is to catch a regression, not to re-litigate
400
+ whether the last change helped.
401
+
402
+ It reports but never gates on `thin_sample`, so it cannot fail a run for being
403
+ small - only the floor is enforced.
404
+
405
+ ## Not measured here
406
+
407
+ - Whether the agent's *answers* got better in general. One task, six runs.
408
+ - Whether the numbers in Results 1 and 2 were caused by the kit. Result 3 shows
409
+ a mechanism that would produce them; the A/B points the same way on a
410
+ different task. Neither window separates the kit from the work.
411
+ - Which part of the kit does the work. Result 3 localises the effect to history
412
+ accumulation, not to any individual rule or plugin.
413
+ - Pruner characters saved, reported from this machine's own `.space/prune.jsonl`
414
+ below rather than from the eval run. It describes one profile on one set of sessions,
415
+ not a population.
416
+ - Whether the free model route is healthy. `oc-harness doctor` answers that and
417
+ needs the network, so it is deliberately outside these scripts.
418
+
419
+ ## Pruner, as recorded by this install
420
+
421
+ `.space/prune.jsonl` is appended by the plugin on every prune. Summarised over
422
+ the 240 records it holds, all on profile `space-bunny-free`:
423
+
424
+ | | |
425
+ |---|---|
426
+ | window | 2026-10-08T01:06Z to 2026-10-11T00:57Z |
427
+ | prunes | 240 |
428
+ | tool outputs stubbed | 3,170 of 7,641 (41.5%) |
429
+ | tool outputs kept | 4,471 |
430
+ | characters saved | 15,437,346 |
431
+ | median / max per prune | 23,388 / 418,510 |
432
+
433
+ Two caveats, because these are not comparable to the A/B numbers above. The
434
+ records come from ordinary working sessions on this machine, not from a fixed
435
+ task, so they describe what the pruner did to real traffic rather than what it
436
+ would do to a controlled workload. And the split timestamp in `measure-install.mjs`
437
+ falls inside this window: 208 of the 240 records and 13,009,568 of those bytes
438
+ post-date the install, so the totals include prunes that ran before the kit was
439
+ installed.
440
+
441
+ Nothing here is regenerated by `npm run measure`, which reads only the session
442
+ database. Rerun with `node -e` over the JSONL, or delete it and let the plugin
443
+ rebuild it.