opencode-harness-kit 0.4.2 → 0.4.4

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +544 -19
  2. package/CONTRIBUTING.md +242 -0
  3. package/GLOBAL.md +34 -5
  4. package/MIGRATION.md +151 -0
  5. package/README.md +294 -37
  6. package/SECURITY.md +30 -0
  7. package/bin/baseline.mjs +15 -7
  8. package/bin/git-hooks/pre-commit +41 -9
  9. package/bin/oc-harness.mjs +1265 -82
  10. package/dist/config.d.ts +127 -0
  11. package/dist/denylist.d.ts +33 -0
  12. package/dist/effort.d.ts +64 -0
  13. package/dist/guard.d.ts +23 -0
  14. package/dist/health.d.ts +43 -0
  15. package/dist/index.d.ts +9 -0
  16. package/dist/index.js +532 -149
  17. package/dist/plugin.d.ts +15 -0
  18. package/dist/profile.d.ts +399 -0
  19. package/dist/project.d.ts +8 -0
  20. package/dist/prune.d.ts +6 -0
  21. package/dist/router.d.ts +3 -0
  22. package/dist/rules.d.ts +109 -0
  23. package/dist/safe.d.ts +7 -0
  24. package/dist/telemetry-store.d.ts +12 -0
  25. package/dist/telemetry.d.ts +3 -0
  26. package/docs/EXIT-CODES.md +71 -0
  27. package/docs/KNOWN-LIMITS.md +45 -21
  28. package/docs/RESEARCH.md +9 -4
  29. package/docs/ROUTING.md +53 -8
  30. package/eval/RESULTS.md +443 -0
  31. package/eval/ab.mjs +743 -0
  32. package/eval/attribute.mjs +170 -0
  33. package/eval/flags.mjs +88 -0
  34. package/eval/measure-install.mjs +323 -0
  35. package/eval/measure-prune.mjs +339 -0
  36. package/eval/metrics.js +79 -0
  37. package/eval/premises.mjs +200 -0
  38. package/eval/prepare.mjs +233 -0
  39. package/eval/run.mjs +376 -0
  40. package/eval/tasks-self.json +81 -0
  41. package/eval/tasks-ts.json +50 -0
  42. package/eval/tasks.json +83 -0
  43. package/eval/thresholds.mjs +134 -0
  44. package/eval/verify.mjs +287 -0
  45. package/package.json +44 -17
  46. package/profiles/fleet-all.json +6 -1
  47. package/profiles/fleet-safe.json +7 -3
  48. package/profiles/schema.json +182 -51
  49. package/prompts/build.md +4 -2
  50. package/skills/space-build/SKILL.md +4 -2
  51. package/skills/space-finish/SKILL.md +6 -2
  52. package/skills/space-plan/reference/task-template.md +13 -5
  53. package/src/config.js +182 -32
  54. package/src/denylist.js +95 -0
  55. package/src/driver/run.mjs +81 -16
  56. package/src/effort.js +15 -4
  57. package/src/guard.ts +165 -13
  58. package/src/health.js +80 -0
  59. package/src/index.ts +15 -4
  60. package/src/lane-usage.js +72 -18
  61. package/src/plugin-type-check.ts +38 -0
  62. package/src/plugin.ts +39 -0
  63. package/src/profile.js +225 -26
  64. package/src/project.js +47 -0
  65. package/src/prune.ts +110 -31
  66. package/src/router.ts +32 -63
  67. package/src/rules.js +99 -7
  68. package/src/safe.ts +27 -0
  69. package/src/sqlite.js +144 -0
  70. package/src/tasks.js +112 -29
  71. package/src/telemetry-store.ts +62 -0
  72. package/src/telemetry.ts +60 -57
  73. package/src/trust.js +70 -9
@@ -0,0 +1,242 @@
1
+ # Contributing to opencode-harness-kit
2
+
3
+ Thanks for looking. This is a small project and the bar for a change is that it
4
+ is either verifiable or it does not land.
5
+
6
+ ## Before you start
7
+
8
+ `npm run check` must pass on an untouched checkout. If it does not, that is a
9
+ bug worth its own issue — do not build on top of a red baseline.
10
+
11
+ `npm run check` is the **offline** gate: build, typecheck, tests. It needs
12
+ nothing but the repository, so it is green on a machine that has never seen an
13
+ OpenCode login.
14
+
15
+ `npm run check:install` is the other half, and it needs a machine that HAS run
16
+ OpenCode: it measures the install database (`sqlite3` and
17
+ `~/.local/share/opencode/opencode.db`), reads the installed config, and
18
+ verifies it. It cannot run on a fresh checkout, and it is not part of
19
+ `npm run check` for that reason. CI runs the offline half; the live half is
20
+ yours to run before a release.
21
+
22
+ ## What a change has to come with
23
+
24
+ The test suite is the contract, so a change without a test is a change nobody
25
+ can trust:
26
+
27
+ - **Model routing, effort tiers, pruning, retention.** Add an assertion to
28
+ `test/`. These tests run offline and must stay offline: no network, no model
29
+ calls, no API keys. `npm test` has to pass on a machine that has never seen
30
+ an OpenCode login.
31
+ - **A new command or flag.** `oc-harness <flag>` that the docs do not mention
32
+ will be found by nobody, and `oc-harness help` listing a command that does not
33
+ exist is worse.
34
+ - **A changed number.** If your change moves a number a profile claims —
35
+ effort, context budget, token budget — the README has to change with it, in
36
+ the same commit. A stale number in the README is a bug, not a nit.
37
+ - **A change to the grader.** The grader decides what the A/B means, and a
38
+ number produced by a grader nobody has read is a number nobody should
39
+ quote. `test/verify.test.js` holds the cases that actually happened.
40
+ - **Anything that writes to disk.** It needs a `--dryrun` path, and it must keep
41
+ a backup. `~/.config/opencode/` is a live config for anyone who runs it,
42
+ including the people reviewing your PR.
43
+
44
+ ## Running the checks
45
+
46
+ ```bash
47
+ npm run check # build + typecheck + tests, offline
48
+ npm run check:install # the above, plus the live install measurement and verify
49
+ npm test # the offline assertions, fast
50
+ npm run typecheck # the library, then the scripts
51
+ npm run typecheck:scripts # only bin/ and eval/
52
+ ```
53
+
54
+ `bin/` and `eval/` are type-checked too, with a looser config than the
55
+ library: they are plain JavaScript with no JSDoc on their parameters, and
56
+ annotating a few hundred of them would be churn rather than work. The loose
57
+ config still catches a wrong type and a missing property on a declared
58
+ shape; it does not catch a property that does not exist on an INFERRED
59
+ object literal - TypeScript does not check those in a `.js` file - so an
60
+ object you build and then add to wants a `@type` on it. The comment in
61
+ `tsconfig.scripts.json` says which is which.
62
+
63
+ `npm run check:install` measures an existing OpenCode install and verifies
64
+ the config in `~/.config/opencode/`. It does not install anything itself, and
65
+ it reads your live config. If you are not comfortable with that over your own
66
+ config, run the layers individually and say so in the PR.
67
+
68
+ `eval/ab.mjs` spawns real agents and costs real tokens. It is deliberately
69
+ not part of any check, and a PR does not need it. `npm run measure` does not
70
+ spawn an agent either — it queries the install database read-only.
71
+
72
+ ## House rules
73
+
74
+ - Node 22 or newer. The CLI uses `node:` prefixed builtins, so an older Node
75
+ fails at import time rather than with a useful message.
76
+ - No new runtime dependencies without a reason that survives review. A CLI that
77
+ installs is one a lot of people will not install twice.
78
+ - `dist/` is committed because the release tarball needs it. It is generated by
79
+ `npm run build`; never hand-edit it, and never commit it without rebuilding.
80
+ - Match the surrounding comment style: comments explain **why**, especially
81
+ where the obvious version would be a trap.
82
+
83
+ ## Pull requests
84
+
85
+ Keep the diff to one concern. Say what you changed, what you measured, and what
86
+ you did not check. If you fixed a bug, the PR should fail before it and pass
87
+ after — say which test does that.
88
+
89
+ The version is set in `package.json` by `npm version`, and the tag is what
90
+ triggers the publish. Keep the two in step: the job publishes the version in
91
+ `package.json`, so a tag that says one version and a file that says another
92
+ is refused by the registry with `EPUBLISHCONFLICT`.
93
+
94
+ ## Cutting a release
95
+
96
+ ```bash
97
+ # 1. the version and the changelog entry
98
+ npm version minor # or major, or patch
99
+ $EDITOR CHANGELOG.md # the entry for the new version
100
+
101
+ # 2. the tag, which is what triggers the publish
102
+ git commit -am "release: <version>"
103
+ git tag -a v<version> -m "<what the release is>"
104
+ git push origin main v<version>
105
+ ```
106
+
107
+ The tag triggers the workflow, the tests run, and the `publish` job publishes
108
+ the tarball. `npm version` writes the version into `package.json`, and the
109
+ publish job publishes the version in `package.json` — so the tag and
110
+ `package.json` have to agree, or the registry refuses the publish with
111
+ `EPUBLISHCONFLICT`. That is the hazard worth guarding: not the bump, but a
112
+ tag that says one version and a file that says another.
113
+
114
+ ### How the publish authenticates
115
+
116
+ Two ways, and the job picks whichever is available:
117
+
118
+ **Trusted publishing (OIDC)** — the default, and what the repository is set up
119
+ for. There is no npm secret in the repository at all: npm exchanges the
120
+ runner's OIDC identity for a short-lived publish token scoped to the workflow.
121
+
122
+ **A token** — the fallback, for when OIDC is not set up. Set the `NPM_TOKEN`
123
+ repository secret to a granular token scoped to `opencode-harness-kit` with
124
+ **Read and write** on the package. If the secret is present the job uses it;
125
+ if it is absent the job uses OIDC.
126
+
127
+ ```bash
128
+ gh secret set NPM_TOKEN --repo pham-canh/space-harness
129
+ ```
130
+
131
+ > **`gh secret set` takes the secret NAME as its first argument**, and the
132
+ > value is pasted at the prompt. `gh secret set <the-token-value>` creates a
133
+ > secret whose *name* is the token - and the name of a secret is visible to
134
+ > everyone who can read the repository's Actions secrets list. If a secret
135
+ > ever shows up named like `npm_...`, delete it and set it again properly.
136
+
137
+ > **A token that can read the package is not necessarily a token that can
138
+ > publish it.** With two-factor authentication on the account, a granular
139
+ > token has to be created with **Bypass two-factor authentication** ticked or
140
+ > the publish is refused with `EOTP - This operation requires a one-time
141
+ > password`, and the status is a plain **401**. `npm whoami` passes, because
142
+ > reading needs no second factor. A 401 on the publish with a working
143
+ > `npm whoami` means the token needs the box ticked, not more permissions.
144
+
145
+ A token needs write access *on the package*, not on the account. npm answers
146
+ **404** for a package the token cannot touch, which reads like a missing
147
+ package rather than a missing permission.
148
+
149
+ ### Configuring the trusted publisher
150
+
151
+ npm's web form has four fields and **npm does not validate what you type into
152
+ it** — a typo is discovered at publish time, and npm reports it as
153
+
154
+ ```
155
+ OIDC token exchange error - package not found
156
+ ```
157
+
158
+ which reads like a missing package. The package is there; the configuration
159
+ is not matching, and npm does not say which field.
160
+
161
+ npm ships `npm trust` for exactly this, and it can also read what is stored.
162
+ So the configuration is set from the command line, where it is compared
163
+ against the identity the workflow will actually present:
164
+
165
+ ```bash
166
+ npm install -g npm@latest # npm trust needs 11.15.0 or later
167
+ npm login # 2FA has to be enabled on the account
168
+ oc-harness trust # check, and replace it if it is wrong
169
+ ```
170
+
171
+ npm completes a two-factor challenge in a **browser**, and the command opens
172
+ the browser for it on the platform it is running on — `open` on macOS,
173
+ `xdg-open` on Linux, `rundll32` on Windows. Answer there however the account
174
+ is set up — an authenticator app, a security key, or a code sent to the
175
+ email. The URL is printed as well, for a machine with no browser to open, and
176
+ the command says which of the two happened rather than claiming the page is
177
+ open when it is not.
178
+
179
+ A code can also be given on the command line, which skips the browser:
180
+
181
+ ```bash
182
+ npx -y opencode-harness-kit@latest trust --otp=123456
183
+ ```
184
+
185
+ The wait for a human is `OC_TRUST_WAIT_SECONDS` (600 by default).
186
+
187
+ `oc-harness trust` reads what npm has stored, compares it against the
188
+ workflow's own identity, and replaces it when it differs. npm allows exactly
189
+ one configuration per package and cannot edit it, so replacing means revoking
190
+ first — the command does that.
191
+
192
+ It works from any directory, which matters: a release tool that only runs
193
+ from one place is a release tool that people run from the wrong one, and the
194
+ failure is `Cannot read package.json`, which says nothing about what the
195
+ command was for.
196
+
197
+ To set it without the script, the command is:
198
+
199
+ ```bash
200
+ npm trust github opencode-harness-kit \
201
+ --file ci.yml \
202
+ --repo pham-canh/space-harness \
203
+ --allow-publish --allow-stage-publish -y
204
+ ```
205
+
206
+ The configuration it writes is:
207
+
208
+ | field | value |
209
+ |---|---|
210
+ | Organization or user | `pham-canh` |
211
+ | Repository | `space-harness` |
212
+ | Workflow filename | `ci.yml` (the filename only, not a path) |
213
+ | Environment name | **empty** |
214
+ | Allowed actions | `npm publish` and `npm stage publish` |
215
+
216
+ **The environment name is the field that catches people.** GitHub only puts an
217
+ `environment` claim in the OIDC token when the job declares one. This
218
+ workflow declares none, so the claim is absent — and a configuration that
219
+ names an environment can never match a token that does not carry one.
220
+
221
+ The workflow is the one in `.github/workflows/ci.yml`, and the claims it
222
+ presents are printed by the publish job before it tries:
223
+
224
+ ```
225
+ repository pham-canh/space-harness
226
+ workflow file ci.yml
227
+ environment (absent) - the trusted publisher must name NO environment
228
+ owner pham-canh
229
+ ```
230
+
231
+ A newly created trusted publisher has to complete its first successful
232
+ publish **within 2 days**, or it expires and has to be created again.
233
+
234
+ > **Both `npm trust` and `npm publish` need a one-time password**, and the
235
+ > web URL they print is masked in the log — so run them in a terminal, where
236
+ > npm prompts for the code. The token path below needs none of that.
237
+
238
+ The GitHub release is written by hand from the CHANGELOG entry:
239
+
240
+ ```bash
241
+ gh release create v<version> --title "<version> - <what it is>" --notes-file <notes>
242
+ ```
package/GLOBAL.md CHANGED
@@ -12,11 +12,31 @@ worse, not better.
12
12
  ## Verify, then claim
13
13
 
14
14
  A task is finished when its verification command passes, not when the code
15
- looks right. Run the project's own test, lint and typecheck command. Paste the
16
- real result, including failures. If you cannot run it, say so plainly.
15
+ looks right. Paste the real result, including failures. If you cannot run it,
16
+ say so plainly.
17
17
 
18
18
  Never invent a test result, a benchmark number, a deploy URL, or a file path.
19
19
 
20
+ **Run the check that covers what you changed, not every check the project
21
+ has.** This is the rule that decides whether the harness is worth having, and
22
+ it was measured:
23
+
24
+ - An absolute rule - "run the project's test, lint and typecheck command" -
25
+ made an agent run the whole suite, and install the dependencies, to rename one
26
+ constant. Across 60 measured runs it was **11.4 bash calls per task against
27
+ 0.2 without it**, and it made the answers *worse*, because every extra
28
+ command is another chance to wander off.
29
+ - The narrow rule is: if the task names a verify command, run **that** one. If
30
+ it does not, run the smallest command that covers the code you touched, and
31
+ say which one you ran. If you cannot tell which covers it, run the whole
32
+ suite once - once, not three times.
33
+ - Do not re-run a check that already passed. One run is the result.
34
+
35
+ **Do not install anything.** Not `npm install`, not `npm ci`, not a package
36
+ the task did not ask for. Installing runs code you did not read, and it
37
+ changes the tree you were asked to change. If the task needs a dependency
38
+ that is not there, say so.
39
+
20
40
  ## Decide, don't ask
21
41
 
22
42
  This is the rule that most changes how a session goes, so it is stated as a
@@ -87,11 +107,20 @@ to avoid this.
87
107
  2. `read` with an `offset`/`limit` for the range you need.
88
108
  3. Read a file whole only when you are about to edit it.
89
109
 
110
+ **When the question already names the file, read it.** The rule above is for
111
+ the case where you do not know where something is. Searching for a file you
112
+ were just told the name of is two extra round trips for the same answer, and
113
+ across 60 measured runs the absolute form cost 3.7 `grep` and 4.8 `read` calls
114
+ a task against 1.5 and 1.2 for the conditional one.
115
+
116
+ Read the whole file when you are going to edit it. Read a range when you are
117
+ going to cite a line.
118
+
90
119
  ## Never
91
120
 
92
- - Force-push, rewrite published history, delete a branch or tag, or publish to
93
- a registry (`npm publish`, `gh release`). Push without being asked; ask before
94
- any of these, because they are the ones with no undo.
121
+ - Force-push, rewrite published history, delete a branch or tag, open a pull
122
+ request, or publish to a registry (`npm publish`, `gh release`). Push without
123
+ being asked; ask before any of these, because they are the ones with no undo.
95
124
  - Commit a secret. If you find one, report it; do not move it.
96
125
  - Read or write `.env`, `.npmrc`, `.netrc`, `.git-credentials`. The guard
97
126
  blocks these and will keep blocking them.
package/MIGRATION.md CHANGED
@@ -1,3 +1,154 @@
1
+ # Migrating
2
+
3
+ Two upgrades change what a user has to do. The most recent one first.
4
+
5
+ - [0.3.x → 0.4.x](#migrating-from-03x-to-04x) — the kit starts enforcing
6
+ things it used to only suggest, and the config says which kit wrote it.
7
+ - [0.2.x → 0.3.0](#migrating-from-02x-to-030) — the single-model profile
8
+ became a fleet.
9
+
10
+ ---
11
+
12
+ # Migrating from 0.3.x to 0.4.x
13
+
14
+ **Nothing you have breaks.** No profile id changed, no file moved, and no
15
+ config field was removed. What changed is that the kit started enforcing
16
+ things it used to only suggest, and it started saying which version wrote
17
+ your config.
18
+
19
+ ## 1. The guard now blocks what it used to allow
20
+
21
+ The denylist was a list of patterns the rules asked the agent to respect.
22
+ It is now checked in the code, before the command runs:
23
+
24
+ | Command | 0.3.x | 0.4.x |
25
+ |---|---|---|
26
+ | `npm publish` | allowed, with a warning in the rules | **refused** |
27
+ | `npm install`, `npm ci` | allowed | **asks first** |
28
+ | `git push` | allowed | **asks first** |
29
+ | `gh pr create`, `gh pr merge` | allowed | **refused** |
30
+
31
+ If one of those was load-bearing in your workflow, the honest answer is
32
+ that it was never going to be the kit's call. Nothing is refused silently:
33
+ the reason is printed, and the command is not run.
34
+
35
+ ## 2. Your config records the kit that wrote it
36
+
37
+ ```jsonc
38
+ "harness": {
39
+ "profile": "fleet-safe",
40
+ "kit": "0.4.4"
41
+ }
42
+ ```
43
+
44
+ Without it, a config from three releases ago and one from today are the same
45
+ file to `verify`, and the only way to tell them apart is to remember. If
46
+ `kit` is missing, your config was written before 0.4.0 and is still valid —
47
+ re-run `oc-harness use <profile>` to bring it up to date.
48
+
49
+ ## 3. The pre-commit hook runs your project's tests, not the kit's
50
+
51
+ This is the change most likely to surprise you. The hook used to run the
52
+ kit's own test suite against the kit's own repository. In a project of
53
+ yours, that suite passed or failed for reasons that had nothing to do with
54
+ you — and it blocked every commit in the process.
55
+
56
+ The hook is now installed only where it belongs:
57
+
58
+ - in the kit's own repository, and
59
+ - in a project of yours, if you ask for it with `oc-harness install --hook`.
60
+
61
+ Where it is installed, it runs **your** project's test script, and only that.
62
+
63
+ ```bash
64
+ oc-harness install --hook
65
+ ```
66
+
67
+ ## 4. The kit works in a directory that is not a git repository
68
+
69
+ The kit used to find the project root through the git worktree. In a
70
+ directory that is not a repository, that lookup returned the filesystem root
71
+ - so the kit found no project, enabled nothing, and reported itself healthy
72
+ while doing nothing at all.
73
+
74
+ The project root is now the directory you are in. If you want it to find
75
+ the repository above you, run the kit from inside it.
76
+
77
+ ## 5. `OPENCODE_HARNESS_PROFILE` names a profile, not a path
78
+
79
+ The variable was checked as if it were a file path, so it never resolved
80
+ and the override never took effect. It names a profile id:
81
+
82
+ ```bash
83
+ export OPENCODE_HARNESS_PROFILE=fleet-safe
84
+ ```
85
+
86
+ ## 6. The guard reads the whole command line
87
+
88
+ The denylist used to be matched against the raw command, so the commands it
89
+ names could be reached through a wrapper. They cannot any more:
90
+
91
+ ```
92
+ cd /tmp && npm publish blocked
93
+ sudo npm publish blocked
94
+ sh -c "npm publish" blocked
95
+ sudo bash -c "npm publish" blocked
96
+ ```
97
+
98
+ The rules the kit already had - matched per segment, with the wrappers
99
+ stripped and the payload of a `bash -c` followed - are the ones the denylist
100
+ uses now. A command that reaches the guard through a shape the kit does not
101
+ know about is still let through, so this is a much taller fence, not a
102
+ proof.
103
+
104
+ ## 7. New commands
105
+
106
+ None of these replace anything. `oc-harness help` lists all of them.
107
+
108
+ | Command | What it is for |
109
+ |---|---|
110
+ | `oc-harness doctor --watch` | Poll the catalogue and report only what changed. Free models are withdrawn in days, and a weekly check is the right rhythm. |
111
+ | `oc-harness upgrade` | Check for a newer kit, refresh the profiles, and write the config again. |
112
+ | `oc-harness cost` | What the kit is doing on THIS project, in numbers: characters pruned, tool outputs stubbed, lanes sorted by cost. |
113
+ | `oc-harness backups`, `oc-harness restore` | The old config is now kept as a dated copy with the profile id in it, and it can be put back. |
114
+ | `oc-harness uninstall` | Remove the symlinks the install created. `npm uninstall` leaves them behind, and opencode then loads neither the rules nor the guard. |
115
+ | `oc-harness doctor --json` | The same report, machine-readable. |
116
+ | `oc-harness show <id> --json` | The resolved profile, machine-readable. |
117
+
118
+ Every command that can fail says so in its exit code, and
119
+ [`docs/EXIT-CODES.md`](docs/EXIT-CODES.md) lists them. The four that matter
120
+ most: `run` exits 1 when there is no task file, 2 when the work needs a
121
+ person, 3 when the agent failed and 4 when the verify failed.
122
+
123
+ ## 8. The effort tier `"none"` means what it says
124
+
125
+ A model that does not publish a `"none"` effort variant no longer gets one
126
+ invented for it. It runs on its own default effort, and the config says
127
+ which model served the request.
128
+
129
+ ## What did not change
130
+
131
+ - The profile ids, the lane table, the plugin hook names (`guard`, `prune`,
132
+ `router`, `telemetry`), and the shape of `retention.yml`.
133
+ - `oc-harness list`, `show`, `install`, `use`, `verify`, `refresh`, `baseline`,
134
+ `lanes` and `trust` keep their arguments.
135
+ - The config format is unchanged. A config written by 0.3.x is read
136
+ correctly by 0.4.x; it is only missing the `kit` field.
137
+
138
+ ## If something looks wrong
139
+
140
+ ```bash
141
+ oc-harness doctor # what the kit sees, and what it would fix
142
+ oc-harness verify # whether the install matches the profile
143
+ oc-harness use fleet-safe && oc-harness install && oc-harness verify
144
+ ```
145
+
146
+ The last line rewrites the config from the profile and checks it. It is
147
+ safe: the old config is written to `~/.config/opencode/backups/` first, and
148
+ `oc-harness restore` puts it back.
149
+
150
+ ---
151
+
1
152
  # Migrating from 0.2.x to 0.3.0
2
153
 
3
154
  0.3.0 replaces the single-model profile with a **fleet**: ten jobs, ten free