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 +49 -0
- package/CONTRIBUTING.md +7 -0
- package/README.md +1 -1
- package/eval/RESULTS.md +7 -7
- package/eval/verify.mjs +28 -4
- package/package.json +1 -1
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 |
|
|
225
|
-
| read calls |
|
|
226
|
-
| input tokens |
|
|
227
|
-
| final context |
|
|
228
|
-
| wall seconds |
|
|
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
|
|
263
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
190
|
-
return
|
|
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.
|
|
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",
|