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 +303 -10
- package/README.md +244 -14
- package/bin/oc-harness.mjs +234 -28
- package/dist/budget.d.ts +247 -0
- package/dist/config.d.ts +34 -1
- package/dist/effort.d.ts +12 -1
- package/dist/guard.d.ts +5 -0
- package/dist/health.d.ts +12 -1
- package/dist/index.js +556 -149
- package/dist/profile.d.ts +2 -1
- package/dist/prune.d.ts +22 -0
- package/dist/rules.d.ts +6 -2
- package/docs/EXIT-CODES.md +18 -4
- package/docs/REVIEW-VERDICT.md +73 -0
- package/eval/RESULTS.md +130 -7
- package/eval/ab.mjs +591 -68
- package/eval/compare.mjs +267 -0
- package/eval/verify.mjs +9 -0
- package/lanes.json +18 -9
- package/package.json +6 -2
- package/profiles/index.json +1 -12
- package/prompts/reviewer.md +24 -0
- package/retention.yml +0 -8
- package/src/budget.js +418 -0
- package/src/config.js +172 -6
- package/src/driver/run.mjs +780 -42
- package/src/effort.js +17 -3
- package/src/guard.ts +304 -1
- package/src/health.js +13 -1
- package/src/index.ts +23 -12
- package/src/profile.js +1 -1
- package/src/prune.ts +21 -1
- package/src/rules.js +145 -0
- package/src/tasks.js +31 -0
package/eval/compare.mjs
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// compare.mjs - grade any agent CLI against the same suite, with the same
|
|
3
|
+
// grader, in a fresh copy of the same subject.
|
|
4
|
+
//
|
|
5
|
+
// eval/ab.mjs measures this kit against opencode, because it has to read
|
|
6
|
+
// opencode's event stream to get token and context numbers. That makes it
|
|
7
|
+
// useless for comparing against Aider, OpenHands or SWE-agent, whose output
|
|
8
|
+
// is a terminal transcript rather than the same JSON.
|
|
9
|
+
//
|
|
10
|
+
// This script takes the other route: it does not care how the agent prints.
|
|
11
|
+
// It runs the command, treats its stdout as the answer, and grades the answer
|
|
12
|
+
// or the resulting tree with eval/verify.mjs - the same citation resolver and
|
|
13
|
+
// the same mutation comparison the kit is measured with. A harness that
|
|
14
|
+
// writes a citation to the right line passes; one that writes prose does not.
|
|
15
|
+
//
|
|
16
|
+
// node eval/compare.mjs --target <dir> --suite eval/tasks-self.json \
|
|
17
|
+
// --reps 3 --timeout 300 \
|
|
18
|
+
// --arm 'aider=aider --yes --no-auto-commits --message "$PROMPT"' \
|
|
19
|
+
// --arm 'swe=python -m sweagent run --problem "$PROMPT"'
|
|
20
|
+
//
|
|
21
|
+
// The command runs through `sh -c` in the run's copy, with:
|
|
22
|
+
// $PROMPT the task text
|
|
23
|
+
// $TASK_ID the task id
|
|
24
|
+
// $DIR the run's copy of the subject
|
|
25
|
+
// $REP 1-based repetition
|
|
26
|
+
// $ARM the arm's name
|
|
27
|
+
//
|
|
28
|
+
// It does NOT install anything. An arm whose binary is not on PATH is
|
|
29
|
+
// reported as unusable and the run exits 2, because a missing harness
|
|
30
|
+
// reported as "0% pass" is a lie about the harness.
|
|
31
|
+
|
|
32
|
+
import { spawnSync } from "node:child_process"
|
|
33
|
+
import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"
|
|
34
|
+
import { tmpdir } from "node:os"
|
|
35
|
+
import { join } from "node:path"
|
|
36
|
+
import { checkPremises } from "./premises.mjs"
|
|
37
|
+
import { gradeAnswer, gradeMutation } from "./verify.mjs"
|
|
38
|
+
|
|
39
|
+
const args = process.argv.slice(2)
|
|
40
|
+
|
|
41
|
+
/** @param {string} name */
|
|
42
|
+
function flag(name) {
|
|
43
|
+
const i = args.indexOf(name)
|
|
44
|
+
if (i < 0) return undefined
|
|
45
|
+
const value = args[i + 1]
|
|
46
|
+
if (value === undefined || value.startsWith("--")) {
|
|
47
|
+
console.error(`${name} needs a value`)
|
|
48
|
+
process.exit(2)
|
|
49
|
+
}
|
|
50
|
+
return value
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @param {string} name */
|
|
54
|
+
function flags(name) {
|
|
55
|
+
const out = []
|
|
56
|
+
for (let i = 0; i < args.length; i++) {
|
|
57
|
+
if (args[i] !== name) continue
|
|
58
|
+
const value = args[i + 1]
|
|
59
|
+
if (value === undefined) {
|
|
60
|
+
console.error(`${name} needs a value`)
|
|
61
|
+
process.exit(2)
|
|
62
|
+
}
|
|
63
|
+
out.push(value)
|
|
64
|
+
}
|
|
65
|
+
return out
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function usage(code) {
|
|
69
|
+
const text = [
|
|
70
|
+
"usage: node eval/compare.mjs --target <dir> --suite <file> --arm name=command [...]",
|
|
71
|
+
"",
|
|
72
|
+
" --target <dir> prepared subject (eval/prepare.mjs)",
|
|
73
|
+
" --suite <file> task suite (eval/tasks-self.json)",
|
|
74
|
+
" --arm name=command repeatable; command runs through sh -c",
|
|
75
|
+
" --reps <n> repetitions per task per arm (default 3)",
|
|
76
|
+
" --timeout <seconds> per run (default 300)",
|
|
77
|
+
" --only <id,id> run only these task ids",
|
|
78
|
+
" --json print one JSON object, and nothing else on stdout",
|
|
79
|
+
].join("\n")
|
|
80
|
+
;(code === 0 ? console.log : console.error)(text)
|
|
81
|
+
process.exit(code)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (args.includes("--help") || args.includes("-h")) usage(0)
|
|
85
|
+
|
|
86
|
+
const target = flag("--target")
|
|
87
|
+
const suitePath = flag("--suite")
|
|
88
|
+
const repsRaw = flag("--reps")
|
|
89
|
+
const timeoutRaw = flag("--timeout")
|
|
90
|
+
const only = flag("--only")?.split(",").map((s) => s.trim()).filter(Boolean)
|
|
91
|
+
const asJson = args.includes("--json")
|
|
92
|
+
const armSpecs = flags("--arm")
|
|
93
|
+
|
|
94
|
+
if (!target || !suitePath || !armSpecs.length) usage(2)
|
|
95
|
+
if (!existsSync(target)) {
|
|
96
|
+
console.error(`target not found: ${target}`)
|
|
97
|
+
process.exit(2)
|
|
98
|
+
}
|
|
99
|
+
if (!existsSync(suitePath)) {
|
|
100
|
+
console.error(`suite not found: ${suitePath}`)
|
|
101
|
+
process.exit(2)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const reps = repsRaw === undefined ? 3 : Number(repsRaw)
|
|
105
|
+
if (!Number.isInteger(reps) || reps < 1) {
|
|
106
|
+
console.error(`--reps must be a positive integer, got ${JSON.stringify(repsRaw)}`)
|
|
107
|
+
process.exit(2)
|
|
108
|
+
}
|
|
109
|
+
const timeout = timeoutRaw === undefined ? 300 : Number(timeoutRaw)
|
|
110
|
+
if (!Number.isInteger(timeout) || timeout < 1) {
|
|
111
|
+
console.error(`--timeout must be a positive integer of seconds, got ${JSON.stringify(timeoutRaw)}`)
|
|
112
|
+
process.exit(2)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const arms = armSpecs.map((spec) => {
|
|
116
|
+
const at = spec.indexOf("=")
|
|
117
|
+
if (at < 1) {
|
|
118
|
+
console.error(`--arm needs name=command, got ${JSON.stringify(spec)}`)
|
|
119
|
+
process.exit(2)
|
|
120
|
+
}
|
|
121
|
+
return { name: spec.slice(0, at), command: spec.slice(at + 1) }
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const suite = JSON.parse(readFileSync(suitePath, "utf8"))
|
|
125
|
+
const premises = checkPremises(target, suite)
|
|
126
|
+
for (const w of premises.warnings) console.error(`warn: ${w.id}: ${w.problem}`)
|
|
127
|
+
if (!premises.ok) {
|
|
128
|
+
console.error(`FAIL: ${premises.fatal.length} premise(s) in ${suitePath} do not hold against ${target}.`)
|
|
129
|
+
for (const f of premises.fatal) console.error(` ${f.id}: ${f.problem}`)
|
|
130
|
+
process.exit(2)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const tasks = suite.tasks.filter((t) => !only || only.includes(t.id))
|
|
134
|
+
if (!tasks.length) {
|
|
135
|
+
console.error("no tasks selected")
|
|
136
|
+
process.exit(2)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// A citation to the suite itself, to a build output or to a test is not
|
|
140
|
+
// evidence about the source. Kept in step with eval/ab.mjs by the comment
|
|
141
|
+
// there; this list is the grading rule, not a per-arm one.
|
|
142
|
+
const SUITE_FILES = [
|
|
143
|
+
"tasks-self.json",
|
|
144
|
+
"tasks.json",
|
|
145
|
+
"tasks-ts.json",
|
|
146
|
+
"dist/",
|
|
147
|
+
// The tests quote the answer, so citing one proves it was grepped, not read.
|
|
148
|
+
"test/",
|
|
149
|
+
"eval/",
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
/** @type {{arm: string, id: string, rep: number, ok: boolean, invalid: boolean, seconds: number, why: string}[]} */
|
|
153
|
+
const rows = []
|
|
154
|
+
|
|
155
|
+
for (let rep = 1; rep <= reps; rep++) {
|
|
156
|
+
for (const task of tasks) {
|
|
157
|
+
for (const arm of arms) {
|
|
158
|
+
// cpSync copies INTO an existing directory. The run copy must be a
|
|
159
|
+
// new path, or the subject lands at <dir>/<basename(target)> and every
|
|
160
|
+
// command that names src/... looks at the wrong tree.
|
|
161
|
+
const base = mkdtempSync(join(tmpdir(), `compare-${arm.name.replace(/[^a-z0-9-]+/gi, "-")}-`))
|
|
162
|
+
const dir = join(base, "subject")
|
|
163
|
+
cpSync(target, dir, { recursive: true })
|
|
164
|
+
const env = {
|
|
165
|
+
...process.env,
|
|
166
|
+
PROMPT: task.task,
|
|
167
|
+
TASK_ID: task.id,
|
|
168
|
+
DIR: dir,
|
|
169
|
+
REP: String(rep),
|
|
170
|
+
ARM: arm.name,
|
|
171
|
+
// opencode (and some wrappers) resolve the project from PWD, which a
|
|
172
|
+
// child inherits from this script rather than from `cwd`.
|
|
173
|
+
PWD: dir,
|
|
174
|
+
}
|
|
175
|
+
const started = Date.now()
|
|
176
|
+
const res = spawnSync(arm.command, {
|
|
177
|
+
shell: true,
|
|
178
|
+
cwd: dir,
|
|
179
|
+
env,
|
|
180
|
+
encoding: "utf8",
|
|
181
|
+
timeout: timeout * 1000,
|
|
182
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
183
|
+
})
|
|
184
|
+
const seconds = (Date.now() - started) / 1000
|
|
185
|
+
|
|
186
|
+
// `sh -c` exits 127 when the command it was given is not on PATH, so
|
|
187
|
+
// the ENOENT is on the shell's stderr rather than on spawnSync's error.
|
|
188
|
+
if (
|
|
189
|
+
(res.error && /** @type {NodeJS.ErrnoException} */ (res.error).code === "ENOENT") ||
|
|
190
|
+
res.status === 127
|
|
191
|
+
) {
|
|
192
|
+
console.error(`arm ${arm.name}: command not found: ${arm.command}`)
|
|
193
|
+
rmSync(base, { recursive: true, force: true })
|
|
194
|
+
process.exit(2)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const timedOut = res.error?.name === "Error" && /ETIMEDOUT|timed out/i.test(res.error.message)
|
|
198
|
+
const invalid = Boolean(res.error) || res.status === null
|
|
199
|
+
let ok = false
|
|
200
|
+
let why = ""
|
|
201
|
+
|
|
202
|
+
if (task.mutate) {
|
|
203
|
+
const g = gradeMutation(dir, task.mutate, { pristine: target })
|
|
204
|
+
ok = g.ok
|
|
205
|
+
why = g.ok ? "tree matches" : g.reason ?? "tree does not match"
|
|
206
|
+
} else {
|
|
207
|
+
const g = gradeAnswer(dir, res.stdout ?? "", task.want, { exclude: SUITE_FILES })
|
|
208
|
+
ok = g.ok
|
|
209
|
+
why = g.ok
|
|
210
|
+
? `cited ${g.cited}`
|
|
211
|
+
: g.cited === 0
|
|
212
|
+
? "no citation"
|
|
213
|
+
: `cited ${g.cited}, none held`
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (timedOut && !ok) why = `timed out after ${timeout}s; ${why}`
|
|
217
|
+
if (invalid && !ok) why = `${res.error?.message ?? "no exit status"}; ${why}`
|
|
218
|
+
|
|
219
|
+
rows.push({ arm: arm.name, id: task.id, rep, ok, invalid, seconds: +seconds.toFixed(1), why })
|
|
220
|
+
if (!asJson) {
|
|
221
|
+
console.log(` ${task.id} ${arm.name}-${rep} ... ${ok ? "PASS" : "fail"}, ${seconds.toFixed(1)}s, ${why}`)
|
|
222
|
+
}
|
|
223
|
+
rmSync(base, { recursive: true, force: true })
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function median(values) {
|
|
229
|
+
if (!values.length) return null
|
|
230
|
+
const s = [...values].sort((a, b) => a - b)
|
|
231
|
+
const m = Math.floor(s.length / 2)
|
|
232
|
+
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const summary = arms.map((arm) => {
|
|
236
|
+
const rs = rows.filter((r) => r.arm === arm.name)
|
|
237
|
+
const valid = rs.filter((r) => !r.invalid)
|
|
238
|
+
return {
|
|
239
|
+
arm: arm.name,
|
|
240
|
+
attempts: rs.length,
|
|
241
|
+
pass: rs.filter((r) => r.ok).length,
|
|
242
|
+
invalid: rs.length - valid.length,
|
|
243
|
+
medianSeconds: median(valid.map((r) => r.seconds)),
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
if (asJson) {
|
|
248
|
+
console.log(JSON.stringify({ target, suite: suitePath, reps, timeout, arms: summary, runs: rows }, null, 2))
|
|
249
|
+
} else {
|
|
250
|
+
console.log("")
|
|
251
|
+
console.log(`model-free comparison: ${tasks.length} task(s) x ${reps} rep(s) x ${arms.length} arm(s)`)
|
|
252
|
+
for (const s of summary) {
|
|
253
|
+
console.log(
|
|
254
|
+
` ${s.arm.padEnd(12)} ${s.pass}/${s.attempts} pass` +
|
|
255
|
+
(s.invalid ? `, ${s.invalid} invalid` : "") +
|
|
256
|
+
(s.medianSeconds === null ? "" : `, median ${s.medianSeconds}s`),
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// A run where every arm crashed measured nothing. Say so rather than
|
|
262
|
+
// printing a 0% that looks like a result.
|
|
263
|
+
const usable = summary.some((s) => s.attempts > s.invalid)
|
|
264
|
+
if (!usable) {
|
|
265
|
+
console.error("no arm produced a usable run")
|
|
266
|
+
process.exit(2)
|
|
267
|
+
}
|
package/eval/verify.mjs
CHANGED
|
@@ -161,6 +161,15 @@ export function resolveInTarget(target, ref) {
|
|
|
161
161
|
const rel = target.replace(/^\/private/, "")
|
|
162
162
|
if (ref.startsWith(rel)) candidates.push(`/private${ref}`)
|
|
163
163
|
if (ref.startsWith(`/private${rel}`)) candidates.push(ref.replace(/^\/private/, ""))
|
|
164
|
+
// A run copy is a byte copy of the snapshot, so a path inside one names
|
|
165
|
+
// the same file relative to the snapshot. This is what makes `--rescore`
|
|
166
|
+
// grade an answer that cited `/tmp/opencode-ab-run-<id>/src/a.ts` after
|
|
167
|
+
// the copy is gone: the live run resolved it against the copy, the
|
|
168
|
+
// re-score has only the snapshot, and without this the same answer reads
|
|
169
|
+
// as "cited 0" - which is how a re-score reported 10/12 where the run it
|
|
170
|
+
// was re-scoring had scored 15/17.
|
|
171
|
+
const copy = /\/opencode-ab-run-[^/]+\/(.+)$/.exec(ref)
|
|
172
|
+
if (copy) candidates.push(join(target, copy[1]))
|
|
164
173
|
} else {
|
|
165
174
|
candidates.push(join(target, ref))
|
|
166
175
|
}
|
package/lanes.json
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
"lanes": {
|
|
5
5
|
"build": {
|
|
6
6
|
"mode": "primary",
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
"description": "Writes and verifies code. Use when the plan is settled and files need to change.",
|
|
8
|
+
"steps": 120,
|
|
9
9
|
"permission": { "edit": "allow" },
|
|
10
10
|
"prompt": "prompts/build.md"
|
|
11
11
|
},
|
|
@@ -69,18 +69,22 @@
|
|
|
69
69
|
"permission": {
|
|
70
70
|
"edit": "deny",
|
|
71
71
|
"bash": {
|
|
72
|
-
"*": "
|
|
72
|
+
"*": "ask",
|
|
73
73
|
"git status *": "allow",
|
|
74
74
|
"git diff *": "allow",
|
|
75
|
-
"mkdir*": "allow",
|
|
76
75
|
"ps*": "allow",
|
|
76
|
+
"node --test*": "allow",
|
|
77
|
+
"npm test*": "allow",
|
|
78
|
+
"npm run *": "allow",
|
|
79
|
+
"npx *": "allow",
|
|
80
|
+
"bun test*": "allow",
|
|
81
|
+
"deno test*": "allow",
|
|
82
|
+
"make*": "allow",
|
|
77
83
|
"python3 -m pytest*": "allow",
|
|
78
84
|
"pytest*": "allow",
|
|
79
85
|
"ruff*": "allow",
|
|
80
86
|
"mypy*": "allow",
|
|
81
87
|
"black*": "allow",
|
|
82
|
-
"npm test*": "allow",
|
|
83
|
-
"npm run *": "allow",
|
|
84
88
|
"npx tsc*": "allow",
|
|
85
89
|
"cargo test*": "allow",
|
|
86
90
|
"go test*": "allow"
|
|
@@ -102,11 +106,16 @@
|
|
|
102
106
|
"permission": {
|
|
103
107
|
"edit": "deny",
|
|
104
108
|
"bash": {
|
|
105
|
-
"*": "
|
|
109
|
+
"*": "deny",
|
|
106
110
|
"pdftotext*": "allow",
|
|
107
|
-
"python3 -c*": "allow",
|
|
108
111
|
"cat*": "allow",
|
|
109
|
-
"ls*": "allow"
|
|
112
|
+
"ls*": "allow",
|
|
113
|
+
"head*": "allow",
|
|
114
|
+
"tail*": "allow",
|
|
115
|
+
"wc*": "allow",
|
|
116
|
+
"file*": "allow",
|
|
117
|
+
"grep*": "allow",
|
|
118
|
+
"rg*": "allow"
|
|
110
119
|
}
|
|
111
120
|
},
|
|
112
121
|
"prompt": "prompts/doc.md"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-harness-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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/opencode-harness-kit#readme",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"typecheck": "tsc --noEmit && npm run typecheck:scripts",
|
|
68
68
|
"typecheck:scripts": "tsc -p tsconfig.scripts.json",
|
|
69
69
|
"pretest": "node -e \"const fs=require('fs');if(!fs.existsSync('test')){console.error('no test directory here - this is an installed package, not a checkout');process.exit(1)}\"",
|
|
70
|
-
"test": "node --test \"test/*.test.js\"",
|
|
70
|
+
"test": "OPENCODE_HARNESS_CATALOG_URL=file://test/fixtures/catalogue.json node --test \"test/*.test.js\"",
|
|
71
71
|
"test:all": "npm run build && npm run typecheck && npm test",
|
|
72
72
|
"test:dist": "git diff --exit-code -- dist/ || (echo \"dist/ is out of sync with src/. Run: npm run build && git add dist/\" && exit 1)",
|
|
73
73
|
"check": "npm run build && npm run test:dist && npm run typecheck && npm test",
|
|
@@ -105,6 +105,10 @@
|
|
|
105
105
|
"esbuild": "^0.28.2",
|
|
106
106
|
"typescript": "^7.0.2"
|
|
107
107
|
},
|
|
108
|
+
"overrides": {
|
|
109
|
+
"zod": "4.6.7",
|
|
110
|
+
"toml": "5.0.1"
|
|
111
|
+
},
|
|
108
112
|
"publishConfig": {
|
|
109
113
|
"access": "public"
|
|
110
114
|
},
|
package/profiles/index.json
CHANGED
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"topEffort": "max",
|
|
111
111
|
"context": 262144,
|
|
112
112
|
"default": false,
|
|
113
|
-
"note": "Open-weight MoE with about 2.5B active parameters for
|
|
113
|
+
"note": "Open-weight MoE, 40B total with about 2.5B active parameters, built for coding agents and small enough to serve locally. Full effort ladder, 262k context, 64k output, no image input. Knowledge cutoff 2026-06. May train on prompts during its free period."
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
"id": "big-pickle",
|
|
@@ -122,17 +122,6 @@
|
|
|
122
122
|
"context": 160000,
|
|
123
123
|
"default": false,
|
|
124
124
|
"note": "Stealth model, no public identity, no effort levels, no image input. The provider may train on it during its free period."
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
"id": "flint-40b-free",
|
|
128
|
-
"file": "profiles/flint-40b-free.json",
|
|
129
|
-
"title": "Flint 40B Free",
|
|
130
|
-
"retention": "may-train",
|
|
131
|
-
"free": true,
|
|
132
|
-
"topEffort": "max",
|
|
133
|
-
"context": 262144,
|
|
134
|
-
"default": false,
|
|
135
|
-
"note": "Open-weight MoE, 40B total with about 2.5B active, built for coding agents and small enough to serve locally. Full effort ladder, 262k context, 64k output, no image input."
|
|
136
125
|
}
|
|
137
126
|
],
|
|
138
127
|
"excluded": [],
|
package/prompts/reviewer.md
CHANGED
|
@@ -38,5 +38,29 @@ Only if pass 1 is clean.
|
|
|
38
38
|
Two sections, pass 1 then pass 2. If pass 1 blocked, pass 2 does not run and
|
|
39
39
|
you say why.
|
|
40
40
|
|
|
41
|
+
## Machine verdict
|
|
42
|
+
|
|
43
|
+
When the request asks for a schema'd verdict (it names
|
|
44
|
+
`oc-harness/review@1`), the two sections are replaced: your final message is
|
|
45
|
+
exactly one JSON object and nothing else, with no code fence and no sentence
|
|
46
|
+
around it.
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"schema": "oc-harness/review@1",
|
|
51
|
+
"verdict": "pass",
|
|
52
|
+
"blockers": [],
|
|
53
|
+
"notes": "short optional note"
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- `pass` means `blockers` is empty. `fail` means at least one blocker.
|
|
58
|
+
- Each blocker is `{ "file": "path/from/repo", "line": 1, "issue": "...",
|
|
59
|
+
"consequence": "..." }`. A blocker without a line and a consequence is not
|
|
60
|
+
actionable and fails validation.
|
|
61
|
+
- Anything that is not this object - prose, a fenced block, a missing field -
|
|
62
|
+
is treated as a failed review by the caller. When in doubt, `fail` with a
|
|
63
|
+
blocker that says what could not be checked.
|
|
64
|
+
|
|
41
65
|
See also the `space-review` skill for the full plan -> build -> review -> finish
|
|
42
66
|
loop and the shared task-file contract.
|
package/retention.yml
CHANGED
|
@@ -110,14 +110,6 @@ models:
|
|
|
110
110
|
verified: 2026-10-19
|
|
111
111
|
source: https://opencode.ai/docs/zen
|
|
112
112
|
|
|
113
|
-
opencode/flint-40b-free:
|
|
114
|
-
retention: may-train
|
|
115
|
-
note: >-
|
|
116
|
-
Free trial endpoint on opencode zen; treated as may-train until terms
|
|
117
|
-
state otherwise.
|
|
118
|
-
verified: 2026-10-22
|
|
119
|
-
source: https://opencode.ai/docs/zen
|
|
120
|
-
|
|
121
113
|
# --- Paid routes, kept because the catalog still serves them and a profile
|
|
122
114
|
# may be pointed at one. ---
|
|
123
115
|
|