opencode-harness-kit 0.5.0 → 0.5.1

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,55 @@ is not happening.
12
12
 
13
13
  ---
14
14
 
15
+ ## [0.5.1] — 2026-10-21
16
+
17
+ Published. The tag is what cut it: `git tag v0.5.1 && git push origin main
18
+ v0.5.1`, and the publish job watches for exactly that.
19
+
20
+ A patch, and it is a patch because nothing a user does differently is in
21
+ here: the grader was stricter than it said, and five tests were reporting
22
+ green with the behaviour they name broken.
23
+
24
+ ### Fixed
25
+
26
+ - **The grader accepted a field the agent added.** The comparison with the
27
+ pristine subject looked at the keys the original had, so an agent that made
28
+ the right change and also added a field of its own was graded correct. The
29
+ added key is a change like any other, and the grader says so.
30
+
31
+ - **`isExcluded` did not normalise the path.** The same file is written
32
+ several ways - "./dist/index.js", "dist/index.js", "src/../dist/index.js" -
33
+ and the check only caught one spelling. The grader was saved by also
34
+ checking the resolved path; the exported helper was not, and it is the
35
+ helper a caller would reach for.
36
+
37
+ ### Test suite
38
+
39
+ Five tests could not fail, and each was found by breaking the code on
40
+ purpose and watching the test stay green:
41
+
42
+ - **"a many-file change that reordered the list" did not test reordering.**
43
+ The fixture put the appended entry first, so a different check caught it
44
+ and the assertion matched both reasons. The comparison with the original -
45
+ the check that matters - was never exercised. There are two tests now.
46
+ - **"the pruner changes nothing when there is no profile" could not fail.**
47
+ The prunable window was empty, and ten DISTINCT outputs all survive
48
+ anyway. It uses repeated outputs now, with a control case.
49
+ - **"a pruner that throws does not break the turn" never reached the
50
+ catch.** A string is iterable in JavaScript, so it yielded characters and
51
+ never threw.
52
+ - **The measurement test did not assert that the measurement measured
53
+ anything.** It ran behind a gate that can never fail and only matched the
54
+ header, so a pruner that measured nothing passed.
55
+ - **The python-fallback test never removed the sqlite3 CLI**, so the
56
+ fallback it was about never ran on a runner that has it.
57
+
58
+ Also fixed: a lane loop that could iterate zero times; two escape hatches
59
+ that made a test assert nothing at all; a git-root test that used the
60
+ checkout and so failed in a source tarball; seventeen temporary directories
61
+ leaked a run; and a profile index read with a fallback that made a missing
62
+ key mean "no entries" rather than a failure.
63
+
15
64
  ## [0.5.0] — 2026-10-21
16
65
 
17
66
  A MINOR release, and the reason is the pruner: the rule that decides what
package/CONTRIBUTING.md CHANGED
@@ -69,6 +69,13 @@ config, run the layers individually and say so in the PR.
69
69
  not part of any check, and a PR does not need it. `npm run measure` does not
70
70
  spawn an agent either — it queries the install database read-only.
71
71
 
72
+ The nightly workflow needs a provider to measure with, and the provider key
73
+ is a **secret** — which a job-level `if` cannot read. The flag is therefore
74
+ a repository **variable**: set `HAS_OPENCODE_KEY` to `true` when
75
+ `OPENCODE_API_KEY` is there, and the job runs. Without the variable the job
76
+ is skipped, not failed, because a schedule that is always red is a schedule
77
+ nobody reads.
78
+
72
79
  `eval/models.mjs` spawns real agents too, one line each, against the live
73
80
  tier: it is what the routing table in `docs/ROUTING.md` is measured with,
74
81
  and it costs a request per model per rep. It is not part of any check
package/README.md CHANGED
@@ -830,7 +830,7 @@ short never grows the history far enough for pruning to matter. So there is
830
830
  a second suite with one task that touches twelve files and asks a question
831
831
  about the code that consumes what it changed — and on it, with the pruner's
832
832
  rule fixed (see below), the kit's arm makes the same number of tool calls
833
- as the arm without the kit and takes 1.2x the wall time.
833
+ as the arm without the kit (47 against 46) and takes 1.2x the wall time.
834
834
 
835
835
  **The pruner used to make the agent re-read.** The first run of that task
836
836
  said the kit was much worse: 86 tool calls against 43 and 282,000 input
package/eval/RESULTS.md CHANGED
@@ -221,11 +221,11 @@ pristine subject file by file, and the answer is graded by citation.
221
221
  | metric (median of 2 runs per arm) | kit on | kit off |
222
222
  | --- | --- | --- |
223
223
  | **correct** | **2/2** | **2/2** |
224
- | tool calls | 51 | 39.5 |
225
- | read calls | 18.5 | 13.5 |
226
- | input tokens | 84,325 | 41,291 |
227
- | final context | 75,705 | 45,647 |
228
- | wall seconds | 123.5 | 104.8 |
224
+ | tool calls | 47 | 46 |
225
+ | read calls | 12 | 18 |
226
+ | input tokens | 78,280 | 37,171 |
227
+ | final context | 68,794 | 42,732 |
228
+ | wall seconds | 121.4 | 104.7 |
229
229
 
230
230
  ### What the long task found, and what it cost to fix
231
231
 
@@ -259,8 +259,8 @@ The context is bigger and the work is not repeated. That is the trade the
259
259
  kit was making badly, and it was making it without saying.
260
260
 
261
261
  **What is left is the instructions, not the pruner.** With the rule on, the
262
- kit's arm makes the same number of tool calls as the arm without the kit
263
- (51 against 39.5, against 67.5 before the fix) and takes 1.2x the wall time.
262
+ kit's arm makes the SAME number of tool calls as the arm without the kit -
263
+ 47 against 46, against 67.5 before the fix - and takes 1.2x the wall time.
264
264
  The remaining cost is the fixed overhead of carrying the kit's rules, and
265
265
  that is the trade the README describes.
266
266
 
package/eval/verify.mjs CHANGED
@@ -180,14 +180,31 @@ export function resolveInTarget(target, ref) {
180
180
  * @returns {boolean}
181
181
  */
182
182
  export function isExcluded(file, exclude = []) {
183
- const base = file.split("/").pop()
183
+ // The path is NORMALISED, because the same file is written several ways:
184
+ // "./dist/index.js", "dist/index.js" and "src/../dist/index.js" are one
185
+ // file, and a check that only catches one spelling is not a check.
186
+ const norm = String(file ?? "")
187
+ .replace(/\\/g, "/")
188
+ .replace(/^\.\//, "")
189
+ .replace(/^\.\//g, "")
190
+ const parts = []
191
+ for (const seg of norm.split("/")) {
192
+ if (!seg || seg === ".") continue
193
+ if (seg === "..") {
194
+ parts.pop()
195
+ continue
196
+ }
197
+ parts.push(seg)
198
+ }
199
+ const clean = parts.join("/")
200
+ const base = parts[parts.length - 1] ?? ""
184
201
  return exclude.some((x) => {
185
202
  if (!x) return false
186
- if (x.endsWith("/")) return file.startsWith(x)
203
+ if (x.endsWith("/")) return clean.startsWith(x)
187
204
  // A file NAME excludes that file wherever it is: the suite is excluded as
188
205
  // "tasks-self.json", and the citation is "eval/tasks-self.json".
189
- if (file === x || base === x) return true
190
- return file.startsWith(`${x}/`)
206
+ if (clean === x || base === x) return true
207
+ return clean.startsWith(`${x}/`)
191
208
  })
192
209
  }
193
210
 
@@ -353,6 +370,13 @@ export function gradeMutationMany(runDir, spec, { pristine } = {}) {
353
370
  return { ok: false, reason: `${file}: ${key} changed, and only ${spec.field} was asked for` }
354
371
  }
355
372
  }
373
+ // And a key the agent ADDED is a change too. Only the keys the original
374
+ // had were compared, so an agent that appended the right entry and also
375
+ // added a field of its own was graded correct.
376
+ for (const key of Object.keys(data)) {
377
+ if (key in orig) continue
378
+ return { ok: false, reason: `${file}: ${key} was added, and only ${spec.field} was asked for` }
379
+ }
356
380
  if (JSON.stringify(arr) !== JSON.stringify(origArr)) changed.push(file)
357
381
  }
358
382
  return { ok: true, changed }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-harness-kit",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Harness kit for OpenCode: capability profiles, a fleet of job lanes with one free model each, role-based reasoning effort, context pruning, and measured baselines.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/pham-canh/space-harness#readme",