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/README.md CHANGED
@@ -20,7 +20,8 @@ git clone https://github.com/pham-canh/opencode-harness-kit.git
20
20
  ```
21
21
 
22
22
  The three steps `init` runs, spelled out for when you want to know what
23
- happened (prefix with `npx` or `./bin/oc-harness.mjs` to taste):
23
+ happened (prefix each with `npx -y opencode-harness-kit@latest` or
24
+ `./bin/oc-harness.mjs` to taste):
24
25
 
25
26
  ```bash
26
27
  oc-harness use fleet-safe # compile the profile
@@ -48,7 +49,7 @@ lanes.json ← the jobs
48
49
  src/ ← no model name anywhere in here
49
50
  ```
50
51
 
51
- Same plugin source, twelve shipped profiles, twelve different behaviours. The
52
+ Same plugin source, eleven shipped profiles, eleven different behaviours. The
52
53
  test suite asserts that rather than taking it on faith.
53
54
 
54
55
  ### How a request travels
@@ -237,7 +238,6 @@ it. One file covers ten jobs either way.
237
238
  | `nemotron-3.5-lightning-free` | 5 | 262,144 | — | **may train** |
238
239
  | `mimo-v2.6-flash-free` | 5 | 200,000 | — | **may train** |
239
240
  | `big-pickle` | 5 | 160,000 | — | **may train** |
240
- | `flint-40b-free` | 5 | 262,144 | `max` | **may train** |
241
241
 
242
242
  The five-lane profiles run everything on one model, which is the right shape
243
243
  when you want one route and want to know exactly which one.
@@ -306,7 +306,7 @@ A CI job that failed on the second kind would go red on a Tuesday because
306
306
  somebody else changed a provider's limits, and a job that goes red for a
307
307
  reason you cannot act on is a job people stop reading.
308
308
 
309
- It is not a hypothetical. The day this was written, four of the twelve
309
+ It is not a hypothetical. The day this was written, four of the shipped
310
310
  shipped profiles had already drifted from the live catalogue — the
311
311
  `temperature` flag had flipped on every one of them.
312
312
 
@@ -560,8 +560,10 @@ oc-harness auto refresh + install + use + verify, asking nothing
560
560
  oc-harness auto --json the same reconcile, as a report a script can read
561
561
  oc-harness use <id> compile a profile into ~/.config/opencode/opencode.jsonc
562
562
  oc-harness use <id> --dryrun print a summary of what would be written, write nothing
563
+ oc-harness use <id> --no-skills leave the skill list out of the lanes' prompt
563
564
  oc-harness refresh <id> re-derive a profile against the live model list
564
- oc-harness install symlink the plugin, prompts, skills and GLOBAL.md
565
+ oc-harness install symlink the plugin, prompts, skills and GLOBAL.md into the config dir
566
+ (from `npx`, they link into a package copy it makes first - see Install)
565
567
  oc-harness link just the plugin, no prompts or skills
566
568
  oc-harness verify check the install against the profile; non-zero on drift
567
569
  oc-harness agents report agents the effort tiering does not reach
@@ -577,7 +579,7 @@ oc-harness uninstall remove the plugin, prompts, skills and GLOBAL.md again
577
579
  oc-harness cost what the kit is doing on this project, in numbers
578
580
  oc-harness restore <file> put one back
579
581
  oc-harness baseline measure the current install
580
- oc-harness run drive .space/tasks.md to done with the build agent
582
+ oc-harness run build and verify .space/tasks.md, one task at a time
581
583
  ```
582
584
 
583
585
  The measurement scripts are in `eval/`, and each one says what it measures
@@ -589,6 +591,7 @@ node eval/measure-prune.mjs the pruner, on real sessions
589
591
  node eval/measure-install.mjs before and after the install
590
592
  node eval/models.mjs every model the profiles use, one line each
591
593
  node eval/ab.mjs the A/B: the same task with and without the kit
594
+ node eval/compare.mjs the same suite against any agent CLI, same grader
592
595
  ```
593
596
 
594
597
  Every flag accepts both spellings: `--profile fleet-safe` and
@@ -857,12 +860,15 @@ One command if you are new here (`init` asks which free profile to run on,
857
860
  then runs the two commands below plus `verify`):
858
861
 
859
862
  ```bash
860
- npx oc-harness init
863
+ npx -y opencode-harness-kit@latest init
861
864
  ```
862
865
 
863
- Two commands, in this order, if you already know which profile you want
864
- (`npx oc-harness …` from the registry, or `./bin/oc-harness.mjs …` from a
865
- checkout — they are the same program):
866
+ Two commands, in this order, if you already know which profile you want.
867
+ Two forms of the same program: `npx -y opencode-harness-kit@latest …` from
868
+ the registry, or `./bin/oc-harness.mjs …` from a checkout. Bare `oc-harness`
869
+ elsewhere in this README is shorthand for whichever form installed it —
870
+ `npx` leaves no global binary, so every command already naming just
871
+ `oc-harness` is one of the two above in disguise:
866
872
 
867
873
  ```bash
868
874
  oc-harness use fleet-safe # writes opencode.jsonc
@@ -873,6 +879,15 @@ oc-harness install # writes everything that file points at
873
879
  `GLOBAL.md` into `~/.config/opencode/`, so this repo stays the single source
874
880
  of truth and an edit here takes effect on the next opencode start.
875
881
 
882
+ That is the checkout behaviour, and it relies on the repository staying put.
883
+ Run through `npx` — or any package install — there is no repository to hold
884
+ the truth, only a npm cache that `npm cache clean --force` can delete under
885
+ it: the installer then COPIES the package once to
886
+ `~/.config/opencode/kit/` and links into that instead, so `npm cache clean`
887
+ cannot dangle the harness out from under its own config. Same commands, same
888
+ config; `uninstall` takes the copy out with the links, and `verify` judges
889
+ both modes by the same ownership rules.
890
+
876
891
  ### The one command that asks nothing
877
892
 
878
893
  `init` is the front door for a person: it asks which free profile to run on.
@@ -1020,6 +1035,20 @@ except the lanes it may hand work to. That is the mechanism behind "hand the
1020
1035
  job to the lane that has it": a lane that is not allowed to spawn anything
1021
1036
  cannot quietly become a general-purpose agent on the most expensive tier.
1022
1037
 
1038
+ Reaching the cap is not an error OpenCode reports: it exits 0 with a text-only
1039
+ answer. `oc-harness run` counts the turns in the transcript, so a verify that
1040
+ failed after the cap says "the agent used its whole 120-step allowance" instead
1041
+ of looking like an ordinary failure. The number quoted is the one in the
1042
+ installed config for the role being run.
1043
+
1044
+ A spent model route is the other way a run looks like a failure and is not
1045
+ one. It shows up as an empty transcript - some providers exit 0 with it, others
1046
+ hang until the ceiling kills them. In the first shape `run` stops with exit 3
1047
+ and "the agent answered nothing" rather than verifying an untouched tree; in
1048
+ the second it names the route and the limit it waited out. Either way the
1049
+ advice is `opencode run --print-logs -m <model> hi`, and a smaller `--budget`
1050
+ fails sooner.
1051
+
1023
1052
  ## The skill loop
1024
1053
 
1025
1054
  The lanes above are the *who*. The skills are the *order*, and they load by
@@ -1041,6 +1070,25 @@ the conversation instead of the requirement.
1041
1070
  Publishing is never a loop step. `space-finish` prepares the change; a human
1042
1071
  decides it ships.
1043
1072
 
1073
+ `oc-harness run` automates the build-and-verify half of that loop, not the
1074
+ whole loop. It hands one task at a time to an agent, re-runs that task's own
1075
+ `verify:` command, and writes the real output back into `.space/tasks.md`. It
1076
+ does not plan, commit or push: those stay with `space-plan`, `space-finish`
1077
+ and the human. A task with no `verify:` is refused rather than assumed done,
1078
+ and so is one whose whole verify line is a command that cannot fail (`true`,
1079
+ `echo ok`). A task that edits files outside its `files:` line stops the run
1080
+ unless `--allow-extra-files` says otherwise. Because that check reads
1081
+ `git status`, `run` itself needs a work tree; outside one it refuses to start
1082
+ rather than run unfenced. `--force-unlock` clears a lock held by a live pid
1083
+ that has been reused by something else.
1084
+
1085
+ `--review` closes the review gap without pretending a sentence is a verdict:
1086
+ after verify passes, the `reviewer` lane is asked for one JSON object matching
1087
+ [`docs/REVIEW-VERDICT.md`](docs/REVIEW-VERDICT.md). A pass marks the task done.
1088
+ A fail, a malformed reply or a crashed reviewer marks it blocked and stops the
1089
+ run with exit 5. The validated verdict is written to `.space/review/`, so the
1090
+ decision can be audited after the fact.
1091
+
1044
1092
  ## What it costs, measured
1045
1093
 
1046
1094
  The kit's own A/B - the same task with and without it, ten reps per arm, 120
@@ -1051,14 +1099,14 @@ so anyone can repeat it:
1051
1099
  |---|---|---|
1052
1100
  | **correct** | **54/60** | **55/60** |
1053
1101
  | tool calls | 12.5 | 8 |
1054
- | input tokens | 56,205 | 10,540 |
1102
+ | uncached input | 56,205 | 10,540 |
1055
1103
  | final context | 39,928 | 12,718 |
1056
1104
  | wall seconds | 43 | 21 |
1057
1105
  | paths written outside the target | 0 | 0 |
1058
1106
 
1059
1107
  **On a task that fits in one read, the kit is a cost.** It spends over five
1060
- times the input tokens and takes twice as long, and the answers are a tie -
1061
- 54 against 55, and a second run of the same 120 runs gave 55 against 54.
1108
+ times the uncached input tokens and takes twice as long, and the answers are a
1109
+ tie - 54 against 55, and a second run of the same 120 runs gave 55 against 54.
1062
1110
  The two edit tasks are a tie at 10/10, and the guard stopped no writes in
1063
1111
  either arm.
1064
1112
 
@@ -1100,6 +1148,97 @@ Whether it is worth it depends on whether the work is long. A repository of
1100
1148
  one-file lookups should not install this. The full table is in
1101
1149
  [eval/RESULTS.md](eval/RESULTS.md).
1102
1150
 
1151
+ ## The token budget
1152
+
1153
+ A free tier is one shared window: every request in every session draws on it,
1154
+ and when it runs out the provider answers with a limit error instead of
1155
+ tokens. opencode never shows a number for it. One real chat session made 599
1156
+ requests and 12,025,980 uncached input tokens; the first sign of trouble was
1157
+ the next request being refused, which reads as a broken model rather than a
1158
+ spent budget.
1159
+
1160
+ The guard counts it. Each `step-finish` part reports what that request cost:
1161
+
1162
+ | field | what it is | counted |
1163
+ |---|---|---|
1164
+ | `input` | the part of the prompt that was not in the provider's cache | yes |
1165
+ | `cache.read` | the cached prefix the model still had to read | yes |
1166
+ | `cache.write` | the prefix stored for the next request | yes |
1167
+ | `output`, `reasoning` | what the model wrote | no |
1168
+
1169
+ The count is the whole context, not `input`. Counting `input` alone looks
1170
+ right and is not: with prompt caching, one measured stream reported 844, 1,289
1171
+ and 391 uncached tokens against 36,480, 7,808 and 9,088 cached ones. A budget
1172
+ built on the first column needed five to forty times the real spend to fire, and
1173
+ the window ran out first - which is exactly what happened. The guard sums the
1174
+ context per session, and at two thresholds it acts:
1175
+
1176
+ | | default | what happens |
1177
+ |---|---|---|
1178
+ | `OC_HARNESS_BUDGET_WARN` | 2,000,000 | one toast: this session has used N context tokens |
1179
+ | `OC_HARNESS_BUDGET_STOP` | 5,000,000 | tool calls are refused, with the number and the fix |
1180
+ | `OC_HARNESS_TURN_STOP` | 300,000 | tool calls are refused after one request re-sends this much: the loop stop |
1181
+
1182
+ ```bash
1183
+ OC_HARNESS_BUDGET_STOP=2000000 OC_HARNESS_TURN_STOP=150000 opencode # a smaller window for a long task
1184
+ OC_HARNESS_BUDGET_WARN=0 OC_HARNESS_BUDGET_STOP=0 OC_HARNESS_TURN_STOP=0 opencode # off
1185
+ ```
1186
+
1187
+ The stop is a refusal with an explanation, not a crash: the model gets the
1188
+ message that the session hit its budget and that a new session is the fix. A
1189
+ subagent gets its own session and its own count, so its tokens are not
1190
+ charged to the conversation that started it.
1191
+
1192
+ ### The window is shared, so a new session does not reset it
1193
+
1194
+ The per-session count is the wrong shape for the thing that actually runs out.
1195
+ The free tier is one window per account: session A spends it, and session B -
1196
+ opened a minute later to get a clean count - is refused on its first request.
1197
+ Two sessions were measured spending one window 47 and 33 minutes apart, so
1198
+ "open a new session" is not a fix once the window is gone.
1199
+
1200
+ The guard therefore keeps a ledger, not just a counter. Every session's spend
1201
+ is written to one file and the window is the sum of the entries inside it:
1202
+
1203
+ | | default | what happens |
1204
+ |---|---|---|
1205
+ | `OC_HARNESS_BUDGET_WINDOW_MINUTES` | 60 | how far back the sum reaches |
1206
+ | `OC_HARNESS_BUDGET_WINDOW_WARN` | 4,000,000 | one toast: the account has used N in the window |
1207
+ | `OC_HARNESS_BUDGET_WINDOW_STOP` | 8,000,000 | tool calls are refused in *every* session, old or new |
1208
+ | `OC_HARNESS_BUDGET_WINDOW_STICKY` | 1 | a crossed stop is recorded and holds until its entries age out (six hours) |
1209
+
1210
+ The ledger is `OC_HARNESS_BUDGET_FILE`, default
1211
+ `$XDG_STATE_HOME/opencode-harness-kit/budget.json` (`~/.local/state/...` when
1212
+ `XDG_STATE_HOME` is unset).
1213
+
1214
+ The file is written atomically (write, then rename); an unreadable file is
1215
+ moved to `budget.json.corrupt` and the window starts empty instead of failing
1216
+ the session or locking it for six hours. If the repair itself cannot be
1217
+ written - a read-only state directory, for example - the call fails closed
1218
+ rather than pretending the window is empty. Entries older than six hours are
1219
+ dropped on the next write. A crossed stop is written into the ledger as a
1220
+ marker, so a session opened after the one-hour sum rolls out is still refused.
1221
+ `OC_HARNESS_BUDGET_WINDOW_STICKY=0` turns that marker off. `0` disables either
1222
+ threshold. The stop is checked when the tool is called, not when the session
1223
+ starts, so a session opened after the window ran out is still refused - with
1224
+ the window's number, not the session's.
1225
+
1226
+ A denial ends the turn: the generated config sets `experimental.continue_loop_on_deny: false`. With it on, opencode fed the guard's refusal back to the model as another turn. The measured session retried six times and made 138 requests / 7,559,016 context tokens in 29 minutes, each retry re-sending the whole prompt. `OC_HARNESS_TURN_STOP` is the second half: it blocks a single request that re-sends more than 300k, while the total still looks small.
1227
+
1228
+ The other half is the fixed cost. opencode writes every discovered skill's
1229
+ name and description into the system message, so a machine with a large skill
1230
+ collection pays tens of thousands of tokens on every request, before the
1231
+ conversation starts. The lanes carry their instructions in `prompts/`, so:
1232
+
1233
+ ```bash
1234
+ oc-harness use fleet-safe --no-skills
1235
+ ```
1236
+
1237
+ omits the skill list from the lanes' prompt and says so in the config header.
1238
+ It is off by default: removing a capability is the user's call, not the kit's.
1239
+
1240
+ It covers the ten lanes. **Subagents are a separate surface**: `--no-skills` does not change a subagent's `skill` tool. It does remove every entry in the generated agent map whose prompt path is `prompts/agents/<name>.txt` - the bundled ECC subagents - because their descriptions were loaded on every request while their prompts were read only when one was delegated to. A lane, or an agent whose prompt is any other path, is left alone; a hand-written agent that reuses the `prompts/agents/` path is removed like the bundle's own. The config header names every entry removed, and the `skill` tool on a subagent left in place is still your call, not the kit's.
1241
+
1103
1242
  ## Known limits
1104
1243
 
1105
1244
  Read [docs/KNOWN-LIMITS.md](docs/KNOWN-LIMITS.md) before relying on failover.
@@ -1167,9 +1306,100 @@ wants a subject it can hand each run a private copy of:
1167
1306
  ```bash
1168
1307
  node eval/prepare.mjs --repo ~/work/lablab-harness --ref 8a674f5
1169
1308
  node eval/ab.mjs --target <printed path> --suite eval/tasks.json --reps 2 --sanitize
1170
- node eval/ab.mjs --rescore <saved stream dir> # re-grade without spawning agents
1309
+ node eval/ab.mjs --target <printed path> --suite eval/tasks.json \
1310
+ --rescore <saved stream dir> # re-grade without spawning agents
1311
+ node eval/premises.mjs --target <printed path> --suite eval/tasks.json
1171
1312
  ```
1172
1313
 
1314
+ `premises` is the freshness check for a suite: it resolves every `file:line`
1315
+ citation the tasks make against the target and exits non-zero on the first one
1316
+ that no longer exists. Run it after a subject moves, before spending runs or
1317
+ trusting a rescore. Point it at this repository and `eval/tasks-self.json` to
1318
+ check the kit's own suite.
1319
+
1320
+ `--fail-fast <n>` (default 2) stops the suite when `n` runs in a row produce
1321
+ no tool call and no text, or when `n` timeouts in a row went quiet well before
1322
+ the per-run ceiling. That is what a spent free route looks like: the run hangs
1323
+ until the per-run ceiling and is recorded as a timeout, so without this a
1324
+ 36-run suite spends hours measuring the provider's quota. A quota error the
1325
+ provider states outright (`Free usage limit reached`, an HTTP 429) stops the
1326
+ suite on the first run instead of waiting for two empty ones, and says so
1327
+ rather than scoring the refusal as a wrong answer. The raw streams are kept
1328
+ either way, so `--rescore <dir>` can still grade them later. The wall time
1329
+ comes from the sidecar, so a run killed at the ceiling is reported at the
1330
+ ceiling, not at the timestamp of its last event.
1331
+
1332
+ Both arms pin `small_model` as well as `model`. `model` only covers the main
1333
+ loop; the title agent runs on `small_model`, whose default is a paid model, so
1334
+ an unpinned kit-off arm makes paid title calls for a benchmark that is meant
1335
+ to be free.
1336
+
1337
+ The kit-off arm has its own `HOME` with no kit config in it, and that is
1338
+ enough to break it: opencode resolves `provider/model` against a catalogue it
1339
+ keeps in `~/.cache/opencode/models.json`, so an arm with a bare `HOME` reports
1340
+ `ProviderModelNotFoundError` for every model the live install knows. The
1341
+ catalogue is opencode's, not the kit's, so `eval/ab.mjs` symlinks the cache
1342
+ into the temporary home along with `~/.local/share/opencode`. If a run still
1343
+ fails before it starts, the error event is now fatal on the spot:
1344
+
1345
+ - `--out <dir>` keeps each run's stream as `<id>-<arm>-<i>.json` next to a
1346
+ `<id>-<arm>-<i>.result.json`, and a rerun with the same `--out` skips the
1347
+ runs that already have a result. A sidecar with no verdict and no step is
1348
+ retried instead of skipped: it is a run that never started, and skipping it
1349
+ would make the hole permanent. A suite interrupted by the quota, or by a
1350
+ reboot, continues instead of starting over. The directory gets a
1351
+ `run-manifest.json`; resuming it against a different target, suite or model
1352
+ is refused rather than mixed into one result.
1353
+ - `--budget <tokens>` caps the suite's context tokens - the same number the
1354
+ guard counts, cached prefix included. It is checked before each run, so the
1355
+ run that crosses the cap is still measured and counted; the next one stops
1356
+ the suite with exit 2.
1357
+ - A preflight run (on by default, `--no-preflight` to skip) asks the model one
1358
+ question before the suite and stops with exit 2 if it does not answer, if it
1359
+ times out, if it exits cleanly with an empty stream, or if its stream carries
1360
+ an error event. It costs one request and
1361
+ saves the eighteen that a broken arm would otherwise spend being scored as
1362
+ wrong answers. Its stream goes to a scratch directory, not `--out`, so
1363
+ `--rescore` does not warn about a file it cannot grade.
1364
+
1365
+ It earns that cost on the day the window is out. `opencode run` then retries
1366
+ the refused request three times and does not exit - one probe sat at zero
1367
+ bytes until a 50-second timeout killed it, and the error text only appears in
1368
+ `--print-logs`. A suite started in that state would have written 120
1369
+ results, every one of them a timeout, and called it a benchmark. The
1370
+ preflight turns it into `exit 2` and no result files. The 10-rep run that
1371
+ produced the numbers in `eval/RESULTS.md` hit this at 47 of 120 runs: the 47
1372
+ are real, the other 73 were never started, and resuming with the same
1373
+ `--out` skips the 47. The window is also account-wide, not per model - a second free
1374
+ model on the same provider answered "Free usage limit reached" too.
1375
+
1376
+ The suite also turns the kit's own budget off for its runs and points the
1377
+ ledger at the run directory. A hundred and twenty runs spend more than the
1378
+ shared window's stop, and from that run on the guard would refuse tool calls -
1379
+ so the runs after it would be scored as wrong answers for a reason the suite
1380
+ is not measuring. The suite has its own cap, and the account's real window is
1381
+ not charged by a benchmark. The cap counts context, not uncached input: 47
1382
+ runs of one 10-rep attempt reported 1,284,594 uncached input tokens while the
1383
+ same streams carried 7,130,164 context tokens, so a cap set against the first
1384
+ number let the suite walk into the provider's wall.
1385
+
1386
+ `eval/ab.mjs` knows how to read opencode's event stream, so it can only compare
1387
+ the kit with opencode. `eval/compare.mjs` grades any CLI the same way: it runs
1388
+ the command in a fresh copy of the subject, treats stdout as the answer, and
1389
+ uses the same citation resolver and mutation comparison. That is what makes a
1390
+ number for Aider or SWE-agent comparable with the kit's own.
1391
+
1392
+ ```bash
1393
+ node eval/compare.mjs --target <prepared path> --suite eval/tasks-self.json \
1394
+ --reps 3 --timeout 300 \
1395
+ --arm 'aider=aider --yes --no-auto-commits --message "$PROMPT"' \
1396
+ --arm 'swe=python -m sweagent run --problem "$PROMPT"'
1397
+ ```
1398
+
1399
+ The command runs through `sh -c` with `$PROMPT`, `$TASK_ID`, `$DIR`, `$REP` and
1400
+ `$ARM` in the environment. A command that is not on PATH stops the run with
1401
+ exit 2 instead of being reported as a 0% pass rate.
1402
+
1173
1403
  `npm test` and `npm run typecheck` need nothing but the repo, so CI runs those.
1174
1404
  The rest read `~/.local/share/opencode/opencode.db` and `~/.config/opencode`,
1175
1405
  which only exist on a machine that has run OpenCode with the kit installed —