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/docs/KNOWN-LIMITS.md
CHANGED
|
@@ -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
|
|
100
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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 `
|
|
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-
|
|
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 |
|
|
54
|
+
| this kit's, current | 1,002 | 19.1% |
|
|
55
55
|
|
|
56
|
-
The
|
|
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
|
|
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 |
|
|
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`
|
|
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
|
|
176
|
-
so the ceiling excludes it. Under `fleet-safe` the lane
|
|
177
|
-
LongCat, which publishes no effort levels at all, and it
|
|
178
|
-
with a 1M context window.
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
package/eval/RESULTS.md
ADDED
|
@@ -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.
|