opencode-harness-kit 0.6.3 → 0.7.0

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 CHANGED
@@ -12,6 +12,294 @@ is not happening.
12
12
 
13
13
  ---
14
14
 
15
+ ## [0.7.0] — 2026-10-25
16
+
17
+ `oc-harness run` now stops for the reasons it already claimed to stop for,
18
+ the read-only lanes no longer have a write-capable shell, and the token budget
19
+ counts the context a request reads instead of only its uncached part.
20
+
21
+ ### Fixed
22
+
23
+ - **A denied tool call was fed back as another turn, and the retry spent the
24
+ window.** `experimental.continue_loop_on_deny` was `true`, so opencode
25
+ handed the guard's refusal back to the model and continued the loop. The
26
+ measured session made 138 requests and 7,559,016 context tokens in 29
27
+ minutes on six `shared token window` denials, each retry re-sending the
28
+ whole prompt. The generated config now sets it to `false`, and the guard
29
+ blocks a single request that re-sends more than `OC_HARNESS_TURN_STOP`
30
+ (default 300k) with "do not retry".
31
+ - **The in-event shared-window stop did not refuse anything.** When the
32
+ `step-finish` sum crossed `OC_HARNESS_BUDGET_WINDOW_STOP`, the guard set
33
+ `windowSpent` but never set `windowWhy`, so `sharedWindowSpent()` returned
34
+ an empty reason and every tool call was allowed. The reason is set where
35
+ the sum crosses, and the stop is written to the ledger with
36
+ `recordStop()` so a new session cannot outlive the provider's window.
37
+ - **A corrupt ledger locked the machine for six hours, or vanished.** It is
38
+ now moved to `budget.json.corrupt` and the window starts empty; the broken
39
+ bytes are kept as evidence. `recordSpend()` and `recordStop()` repair
40
+ before writing instead of reading the corruption as an empty window.
41
+ - **The bundled ECC subagents were loaded on every request.** Their
42
+ descriptions came from `prompts/agents/*.txt`, were not lanes, and so were
43
+ not covered by the stale-lane rule. They are removed from the generated
44
+ agent map when that is the prompt, and named in the config header; a
45
+ hand-written agent that reuses that prompt path is removed like the
46
+ bundle's own, while any other prompt is left alone.
47
+ - **The token budget undercounted a cached session five to forty times, so it
48
+ never fired.** A `step-finish` part reports `input`, `cache.read`,
49
+ `cache.write`, `output` and `reasoning`. The guard summed `input` alone -
50
+ the part of the prompt that was not cached - while the cached prefix, which
51
+ is what a long conversation actually re-reads, was ignored. One measured
52
+ stream reported 844 uncached tokens against 36,480 cached ones on the same
53
+ step, then 391 against 9,088. The 2M warn and 5M stop were therefore five
54
+ to forty times the real spend, and the provider's window ran out before
55
+ either fired. `requestTokens()` in `src/budget.js` now counts `input + cache.read +
56
+ cache.write` for both the session total and the shared-window ledger; the
57
+ toasts and refusals say "context tokens" because that is what they count.
58
+ - **The A/B's `--budget` had the same undercount.** The suite cap summed
59
+ `inputTokens`, so 120 runs reported about 1.3M of spend while their streams
60
+ carried 7.1M. `parseStream` now also returns `contextTokens`, the cap and the
61
+ report use it, and `--rescore` re-reads the old streams to put the corrected
62
+ number on them. `inputTokens` is still recorded so a table can be compared
63
+ with the ones written before this.
64
+ - **The test suite wrote to the real window ledger.** `guard-hook.test.js`
65
+ exercised the guard with the default `OC_HARNESS_BUDGET_FILE`, so every test
66
+ run added its 1,500 and 600 tokens to
67
+ `~/.local/state/opencode-harness-kit/budget.json` - the live ledger showed
68
+ twelve test entries and nothing else. The tests now point the ledger at
69
+ their own temporary directory.
70
+ - **`--rescore` graded a timeout as a wrong answer.** A run that hit the
71
+ ceiling has a stream that stops mid-sentence, which reads exactly like an
72
+ answer that cited nothing; only the sidecar knows which it was. Four runs
73
+ that timed out at 12 steps were re-scored as four wrong answers, and the
74
+ table then showed 18/23 where the honest count is 18/19.
75
+ - **A resume crashed the report on a sidecar from an older version.** The
76
+ re-score reads `escaped`, `tools` and `text` from every result; a sidecar
77
+ written before those fields existed threw a `TypeError` after every run had
78
+ already been paid for. Missing fields are read as empty, and the corrected
79
+ token count is written back so a second resume does not depend on the stream
80
+ still being there.
81
+ - **The A/B's preflight said "ok" to a run that had failed.** It checked only
82
+ the timeout and the quota, so a run that reported an error event and exited
83
+ in three seconds passed. The first suite with that preflight spent eighteen
84
+ runs learning that every kit-off arm had failed with
85
+ `ProviderModelNotFoundError`, scored as "no output, not scored". It now
86
+ reads the stream, and any error event stops the suite with exit 2 and the
87
+ provider's own message. An exit 0 with zero steps and no text is the same
88
+ shape - a spent route - and is refused the same way.
89
+ - **The kit-off arm could not resolve the model it was told to use.** opencode
90
+ keeps its model catalogue in `~/.cache/opencode/models.json`, and the arm's
91
+ temporary `HOME` had no cache, so a bare `model:` string was a model the
92
+ installation had never heard of. The cache is opencode's, not the kit's, so
93
+ it is symlinked into the temporary home the same way `~/.local/share/opencode`
94
+ already was.
95
+ - **An error in a measured run was counted as a wrong answer.** A run that
96
+ produced no tool call and no text, but did report an error, now stops the
97
+ suite at once with that error instead of waiting for `--fail-fast` and then
98
+ scoring the refusal.
99
+ - **The lane counter's "no telemetry" test failed on a machine that had one
100
+ two directories up.** The lookup walks up to the project root by design, so
101
+ `/tmp/.space/telemetry.jsonl` - written by any `opencode run` started in
102
+ `/tmp` - made it find a file and fail for a reason that was not the code's.
103
+ The test now runs twelve levels down, past the walk-up's ceiling, so nothing
104
+ above its own tree is looked at; the walk-up itself is unchanged.
105
+ - **The A/B was governed by the kit's own budget.** The kit-on arm runs on the
106
+ real config, so the guard's session and window thresholds applied to it: 120
107
+ runs spend more than the window's stop, and from the run that crossed it the
108
+ guard refused tool calls - every later run would be scored as a wrong answer
109
+ for a reason the suite is not measuring. The suite now sets all four
110
+ thresholds to `0` for its runs and points `OC_HARNESS_BUDGET_FILE` at the run
111
+ directory, so a benchmark neither trips the guard nor charges the account's
112
+ real window. The suite's own cap is `--budget`.
113
+ - **An empty plan reported success.** A plan with no tasks printed
114
+ "every task is done (0/0)" and exited 0. Nothing was done, so it now stops
115
+ with exit 2 and says the plan has no tasks.
116
+ - **A blocked task did not stop the tasks after it.** The loop marked the
117
+ blocker and carried on, then reported it at the end. It now runs in file
118
+ order and stops at the first blocked task, so pending work before the
119
+ blocker still lands and work after it no longer builds on a failure.
120
+ - **The task file was written back from the copy read at the start.** A
121
+ human edit or an agent deletion made mid-run was overwritten, and a deleted
122
+ task file was recreated. Write-back re-reads the file, refuses to write a
123
+ stale copy, and goes through `rename` so a reader never sees half a plan.
124
+ - **A task was matched by title and position only.** With two same-titled
125
+ tasks, removing or renaming the first made the loop mark the second one
126
+ done. Matching now uses title + `change` + `files`, and an ambiguous match
127
+ stops the run instead of guessing.
128
+ - **`verify: true` and `verify: echo ok` were treated as checks.** Both exit
129
+ 0 whatever the agent did. A verify line made only of no-ops now blocks the
130
+ task before the agent runs; a no-op before a real command
131
+ (`true && npm test`) is still accepted.
132
+ - **`doc` and `verify` opened their bash maps with `"*": "allow"`.** The
133
+ read-only lanes could write through `sed -i`, `awk` or `python3 -c`
134
+ regardless of their `edit: "deny"`. `doc` now defaults to deny and `verify`
135
+ defaults to ask, each with the commands its job actually needs.
136
+ - **A run that hit the lane's step cap reported only "verify failed".** At the
137
+ cap OpenCode exits 0 with a text-only answer, so the loop saw a failed
138
+ check and blamed the change, while the log held the real reason. `run` now
139
+ counts `step_finish` events in the transcript and, when the count reaches
140
+ the role's configured cap, says so in the stop message and in the task's
141
+ recorded output, with the setting to raise.
142
+ - **An agent that answered nothing was reported as a failed change.** A spent
143
+ model route shows up in one of two shapes. Some providers exit 0 with an
144
+ empty stdout and the reason on stderr: the loop then ran the verify against
145
+ an untouched tree and blocked the task for work the model never attempted.
146
+ Others hang until the ceiling kills them, which at the default budget is two
147
+ hours spent on one task with nothing to show for it.
148
+ `run` now stops with exit 3 when stdout is empty without running the verify,
149
+ and a timeout names the route and the limit instead of saying the agent
150
+ "failed". The task is marked blocked either way, with the stderr and the
151
+ advice in its output.
152
+ - **The A/B ground through a quota error as if it were an answer.** The free
153
+ tier can print `{"type":"error"}` with "Free usage limit reached" on stdout
154
+ and exit 0; the suite read that as an empty stream and needed `--fail-fast`
155
+ runs before stopping. It now stops on the first one with exit 2 and the
156
+ provider's own words, because the refusal measures the provider and not the
157
+ kit.
158
+ - **The kit-off arm made paid title calls.** `makeNokitHome` pinned `model`
159
+ but not `small_model`, so the title agent used opencode's default paid
160
+ model. A real run made 1,073 title calls to `gpt-5.4-nano`; every one was
161
+ refused for insufficient funds. Both are pinned now.
162
+ - **A killed A/B left the agent running.** `spawnSync` blocks the event loop,
163
+ so Ctrl-C killed the parent and the child was reparented to systemd - one
164
+ survived 2h54m against the real auth store. The capture uses `spawn`, a
165
+ process group, a timeout timer that kills the group, and `SIGINT`/`SIGTERM`
166
+ handlers that do the same.
167
+ - **`run` started without `opencode`, and outside a git work tree.** The
168
+ first was reported as a failed agent that blocked the first task; the
169
+ second silently switched off the `files:` scope check. Both are now
170
+ refused before the lock is taken, the second unless `--allow-extra-files`
171
+ is passed.
172
+ - **The scope check counted the loop's own bookkeeping as an out-of-scope
173
+ edit.** The plugin writes `.space/telemetry.jsonl` on every agent call, so
174
+ a task that touched only the files it named was still blocked for changing
175
+ `.space/`. Loop state is excluded now; a real edit outside `files:` is
176
+ still caught.
177
+
178
+ - **The A/B's wall-time row read the stream, not the ceiling.** A run killed
179
+ at 180s has a stream that stops at 58s, so the report printed it as a 58s
180
+ run and the wall column favored whichever arm was killed sooner. Each
181
+ sidecar now records the harness's own timer, `--rescore` prefers it, and a
182
+ resumed sidecar without one falls back to the stream's last event.
183
+
184
+ ### Added
185
+
186
+ - **A per-session token budget, counted by the guard.** The free tier is one
187
+ shared window and opencode shows no number for it, so a long session spends
188
+ it silently - one made 599 requests and 12,025,980 input tokens, and the
189
+ first sign was the provider refusing the next request. The guard sums the
190
+ input tokens each `step-finish` part reports, warns once at
191
+ `OC_HARNESS_BUDGET_WARN` (default 2M) and refuses tool calls at
192
+ `OC_HARNESS_BUDGET_STOP` (default 5M) with the number and the fix. `0`
193
+ disables either. The count is per session, so a subagent is not charged to
194
+ the conversation that started it.
195
+ - **`oc-harness use --no-skills`.** opencode writes every discovered skill's
196
+ name and description into the system message, so a large skill collection
197
+ is a fixed cost on every request. The flag turns the skill tool off for the
198
+ lanes, which carry their instructions in `prompts/`, and the config header
199
+ says so. Off by default.
200
+ - **A shared-window ledger, so a new session does not reset the budget.** The
201
+ per-session count answered the wrong question: the free tier is one window
202
+ per account, and two sessions were measured spending one window 47 and 33
203
+ minutes apart. The guard now writes each session's spend to
204
+ `OC_HARNESS_BUDGET_FILE` (default `$XDG_STATE_HOME/opencode-harness-kit/budget.json`)
205
+ and sums the entries inside `OC_HARNESS_BUDGET_WINDOW_MINUTES` (default 60),
206
+ warning at `OC_HARNESS_BUDGET_WINDOW_WARN` (4M) and refusing tool calls at
207
+ `OC_HARNESS_BUDGET_WINDOW_STOP` (8M) in every session, including one opened
208
+ after the window ran out. The file is written atomically; an unreadable file
209
+ is moved to `budget.json.corrupt` and the window starts empty (if the move
210
+ cannot be written, the call fails closed), and entries older than six hours
211
+ are pruned.
212
+ - **`eval/ab.mjs --out <dir>`**: one stream and one result per run, and a
213
+ rerun with the same `--out` skips the runs that already have a result. A
214
+ suite stopped by the quota or by a reboot resumes instead of starting over.
215
+ The directory also gets `run-manifest.json` with the suite hash, target
216
+ fingerprint and model; a resume whose manifest differs stops with exit 2
217
+ instead of mixing two experiments, and a directory from before the manifest
218
+ existed is adopted with a warning.
219
+ - **`eval/ab.mjs --budget <tokens>`**: cap the suite's input tokens. Checked
220
+ before each run, so the run that crosses the cap is still measured; the next
221
+ one stops the suite with exit 2.
222
+ - **A preflight run in `eval/ab.mjs`** (on by default, `--no-preflight` to
223
+ skip): one question to the model before the suite, exit 2 if it does not
224
+ answer, times out, or its stream carries an error event. Answering is the
225
+ test: exit 0 with zero steps and no text stops the suite too.
226
+ - `oc-harness run --review`: after verify passes, ask the reviewer lane for
227
+ a schema'd verdict, `oc-harness/review@1`. A fail, prose, a malformed
228
+ object or a crashed reviewer stops the run with exit 5 and marks the task
229
+ blocked. The verdict is kept at `.space/review/<round>-<slug>.json`; the
230
+ contract is in `docs/REVIEW-VERDICT.md`.
231
+ - `.space/run.lock`: one writer per task file. A lock whose pid is gone is
232
+ cleared; a live one is not. `--force-unlock` breaks one explicitly.
233
+ - `run --allow-extra-files`: documented escape hatch for the scope check.
234
+ - `eval/compare.mjs`: grade any agent CLI against the same suite with the same
235
+ citation resolver and mutation comparison. `eval/ab.mjs` can only compare
236
+ the kit with opencode, because it reads opencode's event stream; this runs a
237
+ plain command in a fresh copy of the subject, so an Aider or SWE-agent
238
+ number is comparable with the kit's own. A command that is not installed is
239
+ reported as unusable, not as a 0% pass rate.
240
+ - `eval/ab.mjs --fail-fast <n>` (default 2): stop when `n` runs in a row produce
241
+ no tool call and no text. A spent free route hangs and times out rather than
242
+ failing, which looked like a benchmark that "stopped after a few minutes"
243
+ when it was really waiting out one dead run at a time. A timeout whose last
244
+ 60 seconds (or a twentieth of the ceiling, whichever is smaller) are quiet
245
+ counts toward the streak too, because a spent route hangs before the
246
+ ceiling. The raw streams are kept for `--rescore`.
247
+
248
+ ### Changed
249
+
250
+ - **The `build` lane's step cap is 120, not 40.** A session that reaches the
251
+ cap is forced into a text-only response, which in an interactive session
252
+ looks like the agent stopping mid-task after a few minutes and in
253
+ `oc-harness run` looks like a task that never finished. 40 steps is a
254
+ reasonable ceiling for one planned task and not for a session that is
255
+ building and testing a change. The other lanes keep their caps.
256
+ - The README and `docs/EXIT-CODES.md` describe `run` as build + verify, not
257
+ plan → build → review → finish. It never planned, committed or pushed.
258
+ - Exit code 5 is now used, and documented.
259
+
260
+ ---
261
+
262
+ ## [0.6.4] — 2026-10-23
263
+
264
+ Fixed, each one found by re-reading what 0.6.3 published, not by running
265
+ anything new:
266
+
267
+ - **The install command in the README named a package that does not exist.**
268
+ The Install section said `npx oc-harness init` "from the registry", and
269
+ `oc-harness` is this kit's binary name, not a package anyone can fetch: the
270
+ command every new reader was pointed at returns `npm error 404 'oc-harness'
271
+ is not in this registry`. Every install path in the README now reads
272
+ `npx -y opencode-harness-kit@latest <cmd>`, bare `oc-harness` is defined
273
+ once as shorthand for one of the two real forms, and a test pins every
274
+ `npx` reference in the README to this package. Released because the npm
275
+ page is a snapshot: the README on npmjs.com renders from the published
276
+ tarball, so fixing it in git changes nothing for the next visitor.
277
+ - **`profiles/index.json` listed `flint-40b-free` twice**, and the README's
278
+ shipped-profiles table repeated it with a second, differently-worded
279
+ note. `oc-harness list` printed twelve rows, `oc-harness init` offered
280
+ twelve choices, flint twice in both, and the README counted twelve
281
+ shipped profiles. Eleven profiles ship; one entry remains, carrying the
282
+ honest note of both. Tests now diff the index, the table and the loader,
283
+ with no Set to collapse the difference.
284
+ - **`retention.yml` repeated `opencode/flint-40b-free`, and the last key
285
+ won.** The hand-rolled parser overwrites `models[id]` on every repeat, so
286
+ the vague 2026-10-22 stub silently replaced the 2026-10-23 record that
287
+ quotes the provider's terms verbatim - same retention class, wrong date,
288
+ lost detail. A test now scans the file for repeated keys.
289
+ - **`install` from npx symlinked into the npx cache.** The installer always
290
+ symlinked the plugin, prompts, skills and GLOBAL.md back to the package
291
+ root - right from a git checkout ("edit here, live everywhere"), visible
292
+ breakage from an npx cache that `npm cache clean --force` deletes. From a
293
+ checkout nothing changes; from npx or any npm install, where `.git` is
294
+ absent, `install` now copies, so the harness on disk does not dangle the
295
+ day the cache is cleaned. `verify` and `uninstall` learn that a
296
+ byte-identical copy of the kit's file is the kit's, and a test installs
297
+ from a copy of the package with no `.git` into a temp HOME, asserts real
298
+ files where links used to go, and runs `verify` green.
299
+
300
+ No runtime change: `dist/`, the plugin and every model-facing artifact are
301
+ byte-for-byte what 0.6.3 shipped.
302
+
15
303
  ## [0.6.3] — 2026-10-23
16
304
 
17
305
  Nothing here changes the runtime. The plugin, the CLI and `dist/` are
@@ -210,15 +498,19 @@ default fleet was carrying a dead fallback rung in **all ten lanes** while
210
498
  release, and it is out of the repository now.
211
499
 
212
500
  A take is about one machine and one moment, and so is the recording of it.
213
- The `.gitignore` already said as much: "a recording stays on the machine
214
- that made it until somebody decides, deliberately, to publish it". The
215
- decision went the other way, so the file went back where that rule keeps
216
- it — `video/build/`, which is ignored — and the README no longer links a
217
- video the repository does not carry.
218
-
219
- What is published is still the old `docs/demo/pitch.mp4`: four minutes
220
- forty-seven seconds, recorded live from a Mac terminal, with a voice. The
221
- npm package never carried a video at all.
501
+ The `.gitignore` of that release said the same thing: "a recording stays on
502
+ the machine that made it until somebody decides, deliberately, to publish
503
+ it". The decision went the other way then, so the file went back where
504
+ that rule keeps it — `video/build/`, which is ignored — and the README no
505
+ longer links a video the repository does not carry. (Neither the rule nor
506
+ the recording survived 0.6.3, which removed the rule along with the
507
+ demonstration.)
508
+
509
+ At the time, what was published on GitHub was still the old
510
+ `docs/demo/pitch.mp4`: four minutes forty-seven seconds, recorded live
511
+ from a Mac terminal, with a voice. 0.6.3 removed it, and the history
512
+ rewrite of that release means no tag carries it today. The npm package
513
+ never carried a video at all.
222
514
 
223
515
  ## [0.5.8] — 2026-10-22
224
516
 
@@ -1868,7 +2160,8 @@ silent in the worst way:
1868
2160
 
1869
2161
  ---
1870
2162
 
1871
- [Unreleased]: https://github.com/pham-canh/opencode-harness-kit/compare/v0.5.7...HEAD
2163
+ [0.7.0]: https://github.com/pham-canh/opencode-harness-kit/compare/v0.6.4...v0.7.0
2164
+ [Unreleased]: https://github.com/pham-canh/opencode-harness-kit/compare/v0.7.0...HEAD
1872
2165
  [0.5.7]: https://github.com/pham-canh/opencode-harness-kit/compare/v0.5.6...v0.5.7
1873
2166
  [0.5.6]: https://github.com/pham-canh/opencode-harness-kit/compare/v0.5.5...v0.5.6
1874
2167
  [0.5.5]: https://github.com/pham-canh/opencode-harness-kit/compare/v0.5.4...v0.5.5