opencode-harness-kit 0.5.7 → 0.5.8
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 +191 -0
- package/README.md +34 -0
- package/bin/baseline.mjs +16 -1
- package/bin/oc-harness.mjs +353 -34
- package/dist/index.js +240 -51
- package/dist/rules.d.ts +6 -0
- package/lanes.json +2 -2
- package/package.json +1 -1
- package/src/config.js +9 -1
- package/src/rules.js +303 -21
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,197 @@ is not happening.
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
## [0.5.8] — 2026-10-22
|
|
16
|
+
|
|
17
|
+
Published. The tag is what cut it: `git tag v0.5.8 && git push origin
|
|
18
|
+
main v0.5.8`, and the publish job watches for exactly that.
|
|
19
|
+
|
|
20
|
+
A minor: one new command, and eleven silent failures closed.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **`oc-harness auto` — the whole reconcile in one command, asking nothing.**
|
|
25
|
+
`refresh`, `install`, `use` and `verify` are each a step somebody has to
|
|
26
|
+
remember, in the right order, with the right flags; an agent that has to
|
|
27
|
+
keep an install working has to know all of that, and has to know which of
|
|
28
|
+
them are allowed to destroy something.
|
|
29
|
+
|
|
30
|
+
The profile is the one the installed config names, or the default. A file
|
|
31
|
+
of yours that the installer would overwrite is **left alone and
|
|
32
|
+
reported**, not overwritten and not asked about — the answer a person
|
|
33
|
+
would give. `--offline` skips the refresh, for a machine that cannot
|
|
34
|
+
reach models.dev. `--json` prints a report a script can read, and nothing
|
|
35
|
+
else on stdout.
|
|
36
|
+
|
|
37
|
+
`--yes` still means consent: a profile whose models may train on your
|
|
38
|
+
prompts is refused without it, exactly as `use` refuses it. The automation
|
|
39
|
+
is about the mechanics, not about the consent.
|
|
40
|
+
|
|
41
|
+
`init` is unchanged. It is the front door for a person, and it still says
|
|
42
|
+
so when there is no terminal to ask on.
|
|
43
|
+
|
|
44
|
+
- **A demo whose numbers are current.** `docs/demo/audit.mp4` — thirty
|
|
45
|
+
seconds, made on a machine with no screen, no camera and no ffmpeg: the
|
|
46
|
+
scene is drawn by a browser, the terminal in it is the real output of the
|
|
47
|
+
real commands, and the narration is on the frame. It names the numbers of
|
|
48
|
+
this release. The older `pitch.mp4` stays where it was — a live recording
|
|
49
|
+
from a Mac, with a voice — and the README now says which is which.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **A refresh overwrote a profile whose backup could not be written.** The
|
|
54
|
+
backup is the only copy of a file the user may have edited by hand — and
|
|
55
|
+
for an npm install that file is inside node_modules, where the next
|
|
56
|
+
`npm install` destroys it anyway. The error was swallowed and the
|
|
57
|
+
profile was overwritten regardless: the user's edits were gone, with no
|
|
58
|
+
backup and no message. A backup that cannot be written now stops the
|
|
59
|
+
refresh.
|
|
60
|
+
|
|
61
|
+
- **`refresh --all` reported profiles it never checked.** A profile that
|
|
62
|
+
could not be parsed was skipped without a word, and the summary counted
|
|
63
|
+
every id it had been given — so a broken profile was reported as a
|
|
64
|
+
refreshed one, and the withdrawn-model check never ran for it. The count
|
|
65
|
+
is of the profiles actually checked, the skipped ones are named, and the
|
|
66
|
+
command exits non-zero: a skipped profile is a failure, not a footnote.
|
|
67
|
+
|
|
68
|
+
- **`generateConfig(profile, { comments: false })` produced a file nothing
|
|
69
|
+
could read.** The whole header is comments, so turning comments off
|
|
70
|
+
emitted it without the `// ` prefix, in front of the JSON body. The
|
|
71
|
+
result was neither JSON nor JSONC, and the kit's own `parseJsonc`
|
|
72
|
+
returned null for it. It is pure JSON now.
|
|
73
|
+
|
|
74
|
+
- **A file named `toString.md` was reported as shadowing a lane.** The
|
|
75
|
+
check was `lane in (actual.agent ?? {})`, and `in` walks the prototype
|
|
76
|
+
chain — so a file named after a property every object has matched a lane
|
|
77
|
+
that does not exist, and `verify` reported a clean install as broken.
|
|
78
|
+
|
|
79
|
+
- **`--role` took the next flag as its value.** `oc-harness run --role
|
|
80
|
+
--max-rounds 5` set the role to the literal string `--max-rounds`, and
|
|
81
|
+
the loop then failed every round and marked every task blocked. A flag
|
|
82
|
+
with no value is refused now, and so is a role that is not a lane —
|
|
83
|
+
while the answer is still "that is not a lane".
|
|
84
|
+
|
|
85
|
+
- **The install deleted a file of yours that only looked like the kit's.**
|
|
86
|
+
The old agent directory was recognised by a substring, and a directory
|
|
87
|
+
called `space-harness/agents-backup` contains that substring. The file
|
|
88
|
+
was deleted. The match is on the parent directory now, segment by
|
|
89
|
+
segment.
|
|
90
|
+
|
|
91
|
+
- **A wrapper the list did not name was a way around the rules.** The
|
|
92
|
+
wrapper list held `env`, `sudo` and a handful of others, so the programs
|
|
93
|
+
that run a command as their argument - `timeout`, `nice`, `ionice`,
|
|
94
|
+
`stdbuf`, `setsid`, `chroot`, `taskset`, `busybox` - were commands the
|
|
95
|
+
rules had never seen. `timeout 5 rm -rf /tmp/x` deleted a tree, and
|
|
96
|
+
`timeout 5 npm publish` published a package, and neither was blocked.
|
|
97
|
+
|
|
98
|
+
The list is a table now, and the flags that take a value are the
|
|
99
|
+
WRAPPER'S OWN: `sudo -n` is a flag and the word after it is the command,
|
|
100
|
+
while `stdbuf -o 0` takes a mode and the word after it is the mode. One
|
|
101
|
+
list for all of them made `sudo -n rm -rf /tmp/x` read `rm` as the value
|
|
102
|
+
of `-n`, so the delete was invisible - which is the same hole in the other
|
|
103
|
+
direction.
|
|
104
|
+
|
|
105
|
+
- **`env -S "rm -rf /tmp/x"` was allowed.** `-S` takes a string and splits
|
|
106
|
+
it into words, so the string is a command line - the same thing `sh -c`
|
|
107
|
+
carries. It used to arrive as ONE token, and the command word was read
|
|
108
|
+
out of it as the text after the last slash, so the guard looked at `x`.
|
|
109
|
+
The payload is read now, and it is read the way `sh -c` is read.
|
|
110
|
+
|
|
111
|
+
- **`git -C/tmp reset --hard` was allowed.** `-C` and `-c` take a value, and
|
|
112
|
+
the value can be written against the flag. The resolver only knew the
|
|
113
|
+
spaced form, so the subcommand it found was the flag itself - and the
|
|
114
|
+
rule that watches for a history rewrite never saw the subcommand at all.
|
|
115
|
+
|
|
116
|
+
- **A public key was refused as a private key.** The rule named the key
|
|
117
|
+
FILES, and `id_rsa.pub` is the public half of `id_rsa` - a file the user
|
|
118
|
+
is meant to publish. Writing one was refused for a reason nobody could
|
|
119
|
+
find.
|
|
120
|
+
|
|
121
|
+
- **A file that was downloaded and then run was ordinary work.** The rule
|
|
122
|
+
caught `curl x | sh` and nothing else, so the same download in two steps
|
|
123
|
+
- fetch it to a file, then run the file - was allowed. It runs exactly the
|
|
124
|
+
same downloaded script.
|
|
125
|
+
|
|
126
|
+
- **A symlink out of the project was a write out of the project.** The
|
|
127
|
+
scope check compared paths as TEXT, so a link inside the project that
|
|
128
|
+
pointed at `/etc` was a path inside the project - and the write landed
|
|
129
|
+
outside it. The comparison is made on the real path now, and the part of
|
|
130
|
+
the path that does not exist yet is put back on the end.
|
|
131
|
+
|
|
132
|
+
- **`oc-harness agents` called a prototype property a lane.** The check was
|
|
133
|
+
`name in (config.agent)`, and `in` walks the prototype chain - so a project
|
|
134
|
+
agent file named `toString.md` or `constructor.md` matched a property every
|
|
135
|
+
object has: the file was reported as shadowing a lane that does not exist,
|
|
136
|
+
and the missing `model:` that was the point of the report was never
|
|
137
|
+
reported either. `verify` had the same check and the same fix; this one
|
|
138
|
+
was left behind.
|
|
139
|
+
|
|
140
|
+
- **`auto --json` dropped everything the refresh had found.** The refresh
|
|
141
|
+
prints what it found - a withdrawn model, a profile it could not read - and
|
|
142
|
+
`auto --json` silences the human output, so those findings went nowhere:
|
|
143
|
+
the report said `"ok": true` while a profile had never been checked
|
|
144
|
+
against the live catalogue, and the exit code said something else
|
|
145
|
+
entirely. The refresh hands its findings back now, and they are in the
|
|
146
|
+
report, in the step's status, and in the exit code.
|
|
147
|
+
|
|
148
|
+
- **`baseline --db=/path` measured the wrong database.** Only the spaced
|
|
149
|
+
form `--db /path` was read, so the `=` spelling - the one every other flag
|
|
150
|
+
in this kit accepts - was ignored, and the default database was measured
|
|
151
|
+
instead. The report then described a database nobody asked about. Both
|
|
152
|
+
spellings work now, and a `--db` with no value is refused instead of
|
|
153
|
+
crashing.
|
|
154
|
+
|
|
155
|
+
- **`doctor --check-models` reported a profile it never checked.** A profile
|
|
156
|
+
that could not be read was skipped without a word, and the run then said
|
|
157
|
+
every shipped model was still in the catalogue - so a profile that was NEVER
|
|
158
|
+
CHECKED was reported as one that was checked and found fine. It is the same
|
|
159
|
+
hole `refresh --all` had, in the one place that runs from a cron. The
|
|
160
|
+
skipped profiles are named now, and the run fails.
|
|
161
|
+
|
|
162
|
+
### Changed
|
|
163
|
+
|
|
164
|
+
- **The `verify` and `doc` lanes ask nothing.** Both ran the project's own
|
|
165
|
+
commands, and both asked before every one of them that the lane's
|
|
166
|
+
allowlist did not name. A run that stops to be asked is a run that does
|
|
167
|
+
not finish. The mechanical safety is unchanged: the guard plugin still
|
|
168
|
+
refuses the denied commands in code, and the lanes still cannot edit.
|
|
169
|
+
|
|
170
|
+
### Added
|
|
171
|
+
|
|
172
|
+
- **A recorder for a machine with no screen.** `vrec` drives the real
|
|
173
|
+
keyboard and the real screen, so it needs macOS, the Screen Recording
|
|
174
|
+
permission, and somebody to grant it. A headless box has none of those,
|
|
175
|
+
and it has no `ffmpeg` either, so the usual encode step has nothing to
|
|
176
|
+
encode with. The published demo was made with `vrec`; on such a machine
|
|
177
|
+
it cannot be made at all.
|
|
178
|
+
|
|
179
|
+
The recorder is in the tools repository, next to `vrec` and `btake` — one
|
|
180
|
+
directory up, and there is exactly one of each, because a copy inside
|
|
181
|
+
this repository is how two copies drift. It draws the scene on a canvas
|
|
182
|
+
and lets the browser be the encoder: Chromium's `captureStream` into
|
|
183
|
+
`MediaRecorder` at a fixed clock, which comes out as an `.mp4` with an
|
|
184
|
+
H.264 track and no ffmpeg on the machine at all. The text on screen is
|
|
185
|
+
the real output of the real commands — the take spec names the commands,
|
|
186
|
+
and they are run when the video is built.
|
|
187
|
+
|
|
188
|
+
The gate is the part that matters. A file that exists, has the right
|
|
189
|
+
duration and has the right size can still be thirty seconds of black, so
|
|
190
|
+
the gate decodes the take, pulls frames out of it, and looks at them: a
|
|
191
|
+
frame with no spread in its pixels shows nothing, and the take fails.
|
|
192
|
+
The frames are written out as PNGs, because a check you cannot look at
|
|
193
|
+
is a check you cannot argue with.
|
|
194
|
+
|
|
195
|
+
**The take is local and the video is local.** The take spec lives in
|
|
196
|
+
`video/takes/` and the recording lands in `video/build/`; both are
|
|
197
|
+
gitignored. The repository ships the recorder and not the take, and a
|
|
198
|
+
recording stays on the machine that made it until somebody decides,
|
|
199
|
+
deliberately, to publish it.
|
|
200
|
+
|
|
201
|
+
And the narration is on the screen. A voice is the best way to carry it
|
|
202
|
+
and it is not always there — `say` is macOS, and a headless Linux box
|
|
203
|
+
has no espeak, no festival and no cloud TTS — so the words go on the
|
|
204
|
+
frame, one caption at a time, and the take still says what it is about.
|
|
205
|
+
|
|
15
206
|
## [0.5.7] — 2026-10-21
|
|
16
207
|
|
|
17
208
|
Published. The tag is what cut it: `git tag v0.5.7 && git push origin main
|
package/README.md
CHANGED
|
@@ -15,6 +15,16 @@ record it and the two mistakes that cost the most time.*
|
|
|
15
15
|
*It was recorded against an earlier version of the kit: the numbers it names
|
|
16
16
|
are the ones from then, and the ones below are the ones now.*
|
|
17
17
|
|
|
18
|
+
<p align="center">
|
|
19
|
+
<video src="docs/demo/audit.mp4" width="720" controls></video>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
*Thirty seconds, made on a machine with no screen and no camera: the scene is
|
|
23
|
+
drawn by a browser, the terminal in it is the **real** output of the **real**
|
|
24
|
+
commands — `oc-harness verify` above — and the narration is on the frame. It
|
|
25
|
+
names the numbers of this release (475 tests), so nothing in it is out of date.
|
|
26
|
+
How it is made is in [`video/`](video/).*
|
|
27
|
+
|
|
18
28
|
A harness kit for [OpenCode](https://opencode.ai) that is **model-agnostic by
|
|
19
29
|
construction**. Switching models is a data edit, not a code change.
|
|
20
30
|
|
|
@@ -375,6 +385,8 @@ keeps a dated copy in `~/.config/opencode/backups/`, including under
|
|
|
375
385
|
oc-harness list profiles with retention and caveats
|
|
376
386
|
oc-harness show <id> resolved lanes, derived numbers, fallback chains, guard rules
|
|
377
387
|
oc-harness init pick a free profile, then install + use + verify
|
|
388
|
+
oc-harness auto refresh + install + use + verify, asking nothing
|
|
389
|
+
oc-harness auto --json the same reconcile, as a report a script can read
|
|
378
390
|
oc-harness use <id> compile a profile into ~/.config/opencode/opencode.jsonc
|
|
379
391
|
oc-harness use <id> --dryrun print a summary of what would be written, write nothing
|
|
380
392
|
oc-harness refresh <id> re-derive a profile against the live model list
|
|
@@ -677,6 +689,28 @@ oc-harness install # writes everything that file points at
|
|
|
677
689
|
`GLOBAL.md` into `~/.config/opencode/`, so this repo stays the single source
|
|
678
690
|
of truth and an edit here takes effect on the next opencode start.
|
|
679
691
|
|
|
692
|
+
### The one command that asks nothing
|
|
693
|
+
|
|
694
|
+
`init` is the front door for a person: it asks which free profile to run on.
|
|
695
|
+
`auto` is the one for a job, an agent, or a script — it reconciles the whole
|
|
696
|
+
install and never waits for an answer:
|
|
697
|
+
|
|
698
|
+
```bash
|
|
699
|
+
oc-harness auto # refresh + install + use + verify
|
|
700
|
+
oc-harness auto --json # the same, as a report a script can read
|
|
701
|
+
oc-harness auto --offline # skip the refresh, for a machine that cannot reach models.dev
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
The profile is the one the installed config names, or the default. A file of
|
|
705
|
+
yours that the installer would overwrite is **left alone and reported**, not
|
|
706
|
+
overwritten and not asked about — the answer a person would give.
|
|
707
|
+
|
|
708
|
+
`--yes` still means consent. A profile whose models may train on your prompts
|
|
709
|
+
is refused without it, exactly as `use` refuses it: the automation is about
|
|
710
|
+
the mechanics, not about the consent. `auto` exits non-zero when `verify`
|
|
711
|
+
finds a problem it is not allowed to repair, and `--json` puts the problems in
|
|
712
|
+
the report.
|
|
713
|
+
|
|
680
714
|
Then restart opencode. Check it took:
|
|
681
715
|
|
|
682
716
|
```bash
|
package/bin/baseline.mjs
CHANGED
|
@@ -16,8 +16,23 @@ import { join } from "node:path"
|
|
|
16
16
|
|
|
17
17
|
const args = process.argv.slice(2)
|
|
18
18
|
const wantJson = args.includes("--json")
|
|
19
|
+
// Both spellings: `--db /path` and `--db=/path`. Only the first was read, so
|
|
20
|
+
// `--db=/path` was ignored and the DEFAULT database was measured instead -
|
|
21
|
+
// and the report said nothing about it. A flag that does not do what it was
|
|
22
|
+
// given is worse than a flag that is missing.
|
|
23
|
+
const dbEq = args.find((a) => a.startsWith("--db="))
|
|
19
24
|
const dbArg = args.indexOf("--db")
|
|
20
|
-
const
|
|
25
|
+
const dbValue =
|
|
26
|
+
dbEq !== undefined
|
|
27
|
+
? dbEq.slice("--db=".length)
|
|
28
|
+
: dbArg >= 0
|
|
29
|
+
? args[dbArg + 1]
|
|
30
|
+
: undefined
|
|
31
|
+
if (dbValue !== undefined && (!dbValue || dbValue.startsWith("--"))) {
|
|
32
|
+
console.error("pass --db <path> or --db=<path>")
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
const DB = dbValue ?? join(homedir(), ".local/share/opencode/opencode.db")
|
|
21
36
|
|
|
22
37
|
const { canQuerySqlite, sqliteMissingMessage, query: queryDb } = await import("../src/sqlite.js")
|
|
23
38
|
|