opencode-harness-kit 0.5.6 → 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 CHANGED
@@ -12,6 +12,229 @@ 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
+
206
+ ## [0.5.7] — 2026-10-21
207
+
208
+ Published. The tag is what cut it: `git tag v0.5.7 && git push origin main
209
+ v0.5.7`, and the publish job watches for exactly that.
210
+
211
+ A patch, and a small one: the shipped profiles are refreshed against the
212
+ live catalogue, and a test that pinned the catalogue's old answer stops
213
+ pinning it.
214
+
215
+ ### Changed
216
+
217
+ - **The profiles are refreshed against the live catalogue.**
218
+ `oc-harness refresh --all` is the kit's own command for this, and it had
219
+ not been run since the 19th. The catalogue now says that
220
+ `minimax-m3.1-flash-free` takes a temperature, and that
221
+ `longcat-2.5-preview-free` does not; the profiles said the opposite in one
222
+ case each. The `verified` date is the day the profile was last checked
223
+ against the catalogue, and leaving it stale after a check makes it a lie.
224
+
225
+ Nothing in the generated config changes: the kit sets no temperature at
226
+ all, so the profile's answer only decides what the header says.
227
+
228
+ ### Fixed
229
+
230
+ - **A test failed on a day when nothing in the kit had changed.** The test
231
+ for "temperature is only emitted for models that support it" asserted
232
+ that one particular model does not support one - a value the catalogue
233
+ owns and changes. The test now asks whether the rule holds: no config the
234
+ kit writes may carry a temperature, and every profile has to say whether
235
+ its model takes one. A test that pins a value the world owns fails for
236
+ the wrong reason.
237
+
15
238
  ## [0.5.6] — 2026-10-21
16
239
 
17
240
  Published. The tag is what cut it: `git tag v0.5.6 && git push origin main
@@ -1437,7 +1660,8 @@ silent in the worst way:
1437
1660
 
1438
1661
  ---
1439
1662
 
1440
- [Unreleased]: https://github.com/pham-canh/opencode-harness-kit/compare/v0.5.6...HEAD
1663
+ [Unreleased]: https://github.com/pham-canh/opencode-harness-kit/compare/v0.5.7...HEAD
1664
+ [0.5.7]: https://github.com/pham-canh/opencode-harness-kit/compare/v0.5.6...v0.5.7
1441
1665
  [0.5.6]: https://github.com/pham-canh/opencode-harness-kit/compare/v0.5.5...v0.5.6
1442
1666
  [0.5.5]: https://github.com/pham-canh/opencode-harness-kit/compare/v0.5.4...v0.5.5
1443
1667
  [0.5.4]: https://github.com/pham-canh/opencode-harness-kit/compare/v0.5.3...v0.5.4
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 DB = dbArg >= 0 ? args[dbArg + 1] : join(homedir(), ".local/share/opencode/opencode.db")
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