opencode-harness-kit 0.0.0-stage → 0.2.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.
Files changed (41) hide show
  1. package/GLOBAL.md +110 -0
  2. package/LICENSE +21 -0
  3. package/README.md +464 -2
  4. package/agents/build.md +66 -0
  5. package/agents/locate.md +48 -0
  6. package/agents/plan.md +61 -0
  7. package/agents/reviewer.md +62 -0
  8. package/agents/verify.md +54 -0
  9. package/bin/baseline.mjs +367 -0
  10. package/bin/git-hooks/pre-commit +30 -0
  11. package/bin/oc-harness.mjs +1025 -0
  12. package/dist/index.js +1038 -0
  13. package/docs/KNOWN-LIMITS.md +188 -0
  14. package/docs/RESEARCH.md +210 -0
  15. package/docs/demo/demo.gif +0 -0
  16. package/docs/demo/demo.mp4 +0 -0
  17. package/docs/demo/render.py +146 -0
  18. package/examples/muse-spark-1.3-contributor-free.json +41 -0
  19. package/package.json +81 -4
  20. package/profiles/deepseek-v4-flash-free.json +41 -0
  21. package/profiles/index.json +78 -0
  22. package/profiles/longcat-2.5-preview-free.json +41 -0
  23. package/profiles/muse-split-free.json +64 -0
  24. package/profiles/schema.json +161 -0
  25. package/profiles/space-bunny-free.json +42 -0
  26. package/profiles/x-preview-f-free.json +41 -0
  27. package/retention.yml +110 -0
  28. package/skills/space-build/SKILL.md +54 -0
  29. package/skills/space-finish/SKILL.md +63 -0
  30. package/skills/space-plan/SKILL.md +101 -0
  31. package/skills/space-plan/reference/task-template.md +47 -0
  32. package/skills/space-review/SKILL.md +53 -0
  33. package/src/config.js +358 -0
  34. package/src/effort.js +98 -0
  35. package/src/guard.ts +59 -0
  36. package/src/index.ts +80 -0
  37. package/src/profile.js +484 -0
  38. package/src/prune.ts +258 -0
  39. package/src/router.ts +192 -0
  40. package/src/rules.js +481 -0
  41. package/src/telemetry.ts +98 -0
package/GLOBAL.md ADDED
@@ -0,0 +1,110 @@
1
+ # Ground rules for every project
2
+
3
+ You are working inside a harness that has already been tuned for the model in
4
+ use. Everything the config and the hooks can enforce is enforced already: the
5
+ tools, the permissions, the command allowlist, the guard, the per-role effort
6
+ and step caps. This file is only what is left over once those are subtracted --
7
+ the judgement calls no plugin can make for you.
8
+
9
+ Read this once. It is short on purpose: long instruction lists get followed
10
+ worse, not better.
11
+
12
+ ## Verify, then claim
13
+
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.
17
+
18
+ Never invent a test result, a benchmark number, a deploy URL, or a file path.
19
+
20
+ ## Decide, don't ask
21
+
22
+ This is the rule that most changes how a session goes, so it is stated as a
23
+ test rather than as a preference.
24
+
25
+ **Decide it yourself and say what you decided, in one line, when you report.**
26
+
27
+ Ask the human only when a wrong guess would:
28
+
29
+ - cost real money, or
30
+ - break the build in a way that cannot be undone, or
31
+ - publish, send, or otherwise make something visible to anyone else, or
32
+ - be irreversible.
33
+
34
+ Everything else is reversible and yours. Guessing at an import path, a
35
+ function name, a file location, a flag, or a test command: look it up and move
36
+ on. If you looked and could not find it, say that.
37
+
38
+ When you are genuinely blocked, ask **one** question, not five, and make it
39
+ answerable with a choice. "Should I use `X` or `Y`" is a question. "What are
40
+ your thoughts on the architecture" is a delay.
41
+
42
+ Never ask what a `grep` would answer.
43
+
44
+ ## Locate before you read
45
+
46
+ On the measured install, `read` ran 2,966 times against 135 `grep` and 114
47
+ `glob` calls — 249 searches, about twelve whole-file reads for every one of
48
+ them. Every production coding harness surveyed uses search-then-read precisely
49
+ to avoid this.
50
+
51
+ 1. `grep` or `glob` to find the candidate files and line numbers.
52
+ 2. `read` with an `offset`/`limit` for the range you need.
53
+ 3. Read a file whole only when you are about to edit it.
54
+
55
+ ## Never
56
+
57
+ - Ask before `git push`, publish, or opening a pull request. Publishing is a human decision.
58
+ - Commit a secret. If you find one, report it; do not move it.
59
+ - Read or write `.env`, `.npmrc`, `.netrc`, `.git-credentials`. The guard
60
+ blocks these and will keep blocking them.
61
+ - Edit a file outside the project directory. Scratch files in the OS temp
62
+ directory are the one exception.
63
+
64
+ The guard plugin enforces the mechanical half of this. It is not a suggestion
65
+ and it does not get quieter when you ask nicely. It blocks the secret files by
66
+ name, refuses writes that escape the project, and unwraps `sudo`, `env`, `git -C`
67
+ and shell wrappers before deciding what a command really does.
68
+
69
+ ## Where things live
70
+
71
+ This file is the map. The repository's own `AGENTS.md`, if present, is more
72
+ specific and wins. Do not restate project rules here.
73
+
74
+ Before you finish a task that changed structure, check whether a document
75
+ now describes something that no longer exists. Stale documentation costs the
76
+ next session more than a small code change.
77
+
78
+ ## Reasoning budget
79
+
80
+ Per-role effort and step caps are set in `opencode.json` and enforced by the
81
+ harness, not by anything written here. `plan` and `reviewer` run at the highest
82
+ available effort, `build` in the middle, `locate` and `verify` at the lowest,
83
+ because finding a thing and checking a thing are not design work. If you are
84
+ playing one of the low-effort roles and feel yourself designing an
85
+ architecture, stop and report the evidence instead.
86
+
87
+ ## When context grows
88
+
89
+ Tool output from earlier in the session is pruned automatically. You keep a
90
+ one-line record of what each command returned, not its full body. If you need
91
+ the body, re-run the command or re-read the range. That is cheaper than
92
+ reasoning over output that has already been summarised away.
93
+
94
+ Never re-run a command purely to get its output back into context. Say what
95
+ you need from it and get only that.
96
+
97
+ ## Model etiquette
98
+
99
+ If a request fails with a rate limit or a usage cap, **stop**. Do not retry the
100
+ same model; free routes often stay blocked for days, and the daily window only
101
+ resets at 00:00 UTC. Report it and ask for a new session. No plugin can swap
102
+ the model of a live session, so a new session is the actual fix.
103
+
104
+ If the model you are on is not the one described in the config, say so before
105
+ starting anything expensive.
106
+
107
+ ## When you are stuck
108
+
109
+ Say what you tried, what you expected, and what happened instead. A precise
110
+ report is more useful than a third attempt at the same approach.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 opencode-harness-kit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,465 @@
1
- # Temporary Holding Version
1
+ # opencode-harness-kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/pham-canh/space-harness/actions/workflows/ci.yml/badge.svg)](https://github.com/pham-canh/space-harness/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/opencode-harness-kit.svg)](https://www.npmjs.com/package/opencode-harness-kit)
5
+
6
+ A harness kit for [OpenCode](https://opencode.ai) that is **model-agnostic by
7
+ construction**. Switching models is a data edit, not a code change.
8
+
9
+ ```bash
10
+ npx oc-harness init # pick a free model, then install + verify
11
+ ```
12
+
13
+ then restart opencode. That is the whole quickstart. No npm account needed on
14
+ your side — `npx` only downloads the published kit. Prefer git? The same three
15
+ steps from a checkout (see [Install](#install)):
16
+
17
+ ```bash
18
+ git clone https://github.com/pham-canh/space-harness.git
19
+ ./bin/oc-harness.mjs init
20
+ ```
21
+
22
+ ![demo: init and verify in one go](docs/demo/demo.gif)
23
+
24
+ Regenerate it any time with `python3 docs/demo/render.py` (needs ffmpeg); the
25
+ frames are drawn from real CLI output, not typed by hand.
26
+
27
+ The three steps `init` runs, spelled out for when you want to know what
28
+ happened (prefix with `npx` or `./bin/oc-harness.mjs` to taste):
29
+
30
+ ```bash
31
+ oc-harness use space-bunny-free # compile the profile
32
+ oc-harness install # link the agents, skills and GLOBAL.md
33
+ oc-harness verify # prove the install is whole
34
+ ```
35
+
36
+ `use` writes the config. `install` writes everything the config *points at*.
37
+ Skipping it is the failure mode this kit is built to prevent: see
38
+ [Install](#install).
39
+
40
+ ## The problem it solves
41
+
42
+ Tuning a coding harness for a model means answering model-specific questions:
43
+ how much context to reserve, which reasoning levels exist, whether temperature
44
+ is even accepted, how much privacy the provider gives you. Answer them in a
45
+ prompt and you get a paragraph the model reads once. Answer them in code and
46
+ you get two projects that share a name and nothing else.
47
+
48
+ This kit puts the answers in a profile:
49
+
50
+ ```
51
+ profiles/space-bunny-free.json ← the model
52
+ src/ ← no model name anywhere in here
53
+ ```
54
+
55
+ Same plugin source, four shipped profiles, four different behaviours. The test
56
+ suite asserts that rather than taking it on faith.
57
+
58
+ ## What the compiler derives
59
+
60
+ Given a profile, `oc-harness use` writes an `opencode.jsonc` with every
61
+ model-dependent number already worked out and commented:
62
+
63
+ ```
64
+ $ oc-harness use deepseek-v4-flash-free --print
65
+
66
+ // profile deepseek-v4-flash-free
67
+ // context 200,000 tokens
68
+ // input cap 200,000 tokens
69
+ // reasoning low < high < max
70
+ // retention zero-retention
71
+ //
72
+ // Derived numbers, do not hand-edit:
73
+ // compaction.reserved 24,000 (12% of the input cap)
74
+ // prune threshold 176,000 (input cap minus reserve)
75
+ // role effort plan=max build=high reviewer=max locate=low verify=low
76
+ // agent steps build=40 plan=25 reviewer=15 locate=12 verify=20
77
+ ```
78
+
79
+ Against a 1M-context model, the same source produces `reserved 62,915` and
80
+ `prune threshold 461,373`. Nobody edits those by hand, and nobody copies a
81
+ 200k-tuned config onto a 1M model and wonders why it overflows.
82
+
83
+ ### Levels are resolved, never emitted raw
84
+
85
+ A role asking for `max` on a model whose top level is `xhigh` gets `xhigh`, and
86
+ the substitution is reported:
87
+
88
+ ```
89
+ $ oc-harness show muse-spark-1.3-contributor-free
90
+ effort levels minimal < low < medium < high < xhigh
91
+ role effort
92
+ plan xhigh (requested max)
93
+ adjustments
94
+ ! role plan asked for max; muse-spark-1.3-contributor-free tops out at xhigh.
95
+ ```
96
+
97
+ A model with no tunable effort at all gets **no** `variant` written, rather than
98
+ a level it does not have.
99
+
100
+ ## Privacy is a first-class field
101
+
102
+ `models.dev` describes capability and price. It does not describe what a
103
+ provider does with your prompts. `retention.yml` is hand-curated for that
104
+ reason, and a profile cannot be installed until a human has stated its terms.
105
+
106
+ ```
107
+ $ oc-harness list
108
+ ▸ space-bunny-free 1,048,576 max zero-retention
109
+ ✗ muse-spark-1.3-contributor-free
110
+ Provider may train on your prompts and completions (Meta contributor
111
+ tier). Top effort is xhigh, not max.
112
+ ```
113
+
114
+ The excluded model is still installable with `--yes`, and the warning repeats
115
+ on install. It is not hidden, it is labelled.
116
+
117
+ ## Shipped profiles
118
+
119
+ | profile | context | top effort | retention |
120
+ |---|---|---|---|
121
+ | `space-bunny-free` (default) | 1,048,576 | `max` | zero-retention |
122
+ | `muse-split-free` | 1,048,576 | `max` | **may train** |
123
+ | `x-preview-f-free` | 1,000,000 | `max` | zero-retention |
124
+ | `longcat-2.5-preview-free` | 1,000,000 | — | zero-retention |
125
+ | `deepseek-v4-flash-free` | 200,000 | `max` | zero-retention |
126
+ | `muse-spark-1.3-contributor-free` | 1,048,576 | `xhigh` | **may train** |
127
+
128
+ ### Split profiles
129
+
130
+ A profile normally puts one model behind every role. `roleModels` is the way to
131
+ split that, for when the best free model and the highest-output model are not the
132
+ same model:
133
+
134
+ ```jsonc
135
+ "model": "space-bunny-free", // base: every role not named below
136
+ "roleModels": {
137
+ "plan": { "model": "muse-spark-1.3-contributor-free", "levels": ["minimal","low","medium","high","xhigh"] },
138
+ "build": { "model": "muse-spark-1.3-contributor-free", "levels": ["minimal","low","medium","high","xhigh"] },
139
+ "locate": { "model": "space-bunny-free", "levels": ["low","medium","high","xhigh","max"] }
140
+ }
141
+ ```
142
+
143
+ Three things fall out of this that are easy to get wrong, so they are enforced:
144
+
145
+ - **`levels` is required when the model differs.** Effort is resolved against the
146
+ levels of the model the role actually runs on. `plan` asking for `max` on a
147
+ model that stops at `xhigh` lands on `xhigh`, instead of writing a `max` variant
148
+ the model has no meaning for.
149
+ - **Retention is the least private model in the file, not the base one.** A base
150
+ of `zero-retention` with a `may-train` role is a `may-train` profile. The
151
+ generated header prints that, with the model responsible, and so do
152
+ `oc-harness use` and `oc-harness show`.
153
+ - **Each role's output cap is its own.** A split role inherits the limits of the
154
+ model it names, so a `build` on a 131,072-token model cannot spend the base's
155
+ 524,288.
156
+
157
+ `muse-split-free` is the shipped example, and it is deliberately not the default:
158
+ three of its five roles run on Muse Spark, a Meta contributor-tier endpoint whose
159
+ provider may train on your prompts. Install it for public or throwaway
160
+ repositories.
161
+
162
+ ## Commands
163
+
164
+ ```
165
+ oc-harness list profiles with retention and caveats
166
+ oc-harness use <id> compile a profile into ~/.config/opencode/opencode.jsonc
167
+ oc-harness show <id> resolved effort, derived numbers, fallback queue
168
+ oc-harness use <id> --dryrun print what would be written, write nothing
169
+ oc-harness install symlink the plugin, agents, skills and GLOBAL.md
170
+ oc-harness verify check the install against the profile; non-zero on drift
171
+ oc-harness agents report agents the effort tiering does not reach
172
+ oc-harness doctor re-check profiles against models.dev and the live config
173
+ oc-harness baseline measure the current install
174
+ ```
175
+
176
+ ### `verify` is the command that matters
177
+
178
+ A generated config is only half an install. The other half is the instruction
179
+ file it names, the agent files it declares, and the plugin that enforces the
180
+ guard — and every one of those can be missing while the config still validates
181
+ and still looks correct.
182
+
183
+ That is not hypothetical. On a real install of this kit, `opencode.jsonc`
184
+ pointed at a `GLOBAL.md` that did not exist, declared three agents whose markdown
185
+ files had never been copied, and had `build` — the *default* agent — hand-pinned
186
+ to a `may-train` model while every generated field still matched the profile.
187
+ Nothing errored. `opencode agent list` simply did not show those agents.
188
+
189
+ So `verify` exits non-zero when the config has drifted from its profile, when an
190
+ instruction path does not resolve, when a tiered role has no agent file, when an
191
+ agent names a model that is not active, or when a stripped schema key is present:
192
+
193
+ ```
194
+ $ oc-harness verify
195
+
196
+ verify profile space-bunny-free -> ~/.config/opencode/opencode.jsonc
197
+
198
+ ✓ config matches profile space-bunny-free
199
+ ✓ instruction ~/.config/opencode/GLOBAL.md resolves
200
+ ✓ plugin linked
201
+
202
+ install is consistent with the profile
203
+ ```
204
+
205
+ `doctor` now reports the same config drift alongside its models.dev check, because
206
+ "the profile is fine" and "the installed config matches the profile" are
207
+ different questions and only one of them was being asked.
208
+
209
+ `doctor` exists because free models get withdrawn within days. It diffs each
210
+ profile against live metadata and reports drift:
211
+
212
+ ```
213
+ ✓ space-bunny-free matches catalog
214
+ ! muse-spark-1.3-contributor-free drift
215
+ input 524288 -> 1048576
216
+ ```
217
+
218
+ That drift was real; `doctor` found it during development.
219
+
220
+ ## Measuring before tuning
221
+
222
+ `oc-harness baseline` reads the OpenCode SQLite database and reports what is
223
+ actually happening, so thresholds come from measurements rather than intuition.
224
+ From one real install:
225
+
226
+ ```
227
+ CONTEXT PER REQUEST
228
+ p50 100,942 p90 256,940 p99 481,687 max 547,986
229
+ fresh input p50 528
230
+ STORED MESSAGE BYTES
231
+ tool 116.58 MB 77.9%
232
+ reasoning 23.10 MB 15.4%
233
+ text 3.12 MB 2.1%
234
+ READ PRESSURE
235
+ read calls 2,966
236
+ grep/glob/list 249
237
+ reads per search 11.9
238
+ reads over 8 KB 415
239
+ cut by the 200-line cap 441
240
+ re-reads with an offset 423
241
+ PERMISSION PRESSURE
242
+ prompt with kit allowlist 1,002 (18.5%)
243
+ compaction events 0 (never fired)
244
+ ```
245
+
246
+ Four numbers in that report changed how this kit is configured, and two of them
247
+ reversed an earlier decision:
248
+
249
+ **Tool output is 78% of stored context**, and sessions re-read ~101k tokens per
250
+ request to add ~528 new ones. That is why pruning here is deterministic and
251
+ unconditional.
252
+
253
+ **Read pressure is 11.9 whole-file reads per search.** Every production harness
254
+ in the study in `docs/KNOWN-LIMITS.md` searches before reading; this one mostly
255
+ did not. That is a prompt problem, not a config problem, and the fix is a rule
256
+ in `GLOBAL.md` and in the agent files rather than a number.
257
+
258
+ **Truncation and re-read are the same number.** 441 reads were cut by the
259
+ 200-line cap and 423 were re-read with an `offset`. Truncation was not saving
260
+ context, it was buying extra round trips at ~17s median apiece — which is why
261
+ `tool_output` now sits at OpenCode's own 2000 / 51200 default. Only 14 of 2,966
262
+ reads exceed 51,200 bytes, so that ceiling is not what limits context.
263
+
264
+ Your numbers stay on your machine; only the script is in the repo.
265
+
266
+ ## The plugins
267
+
268
+ | module | hook | job |
269
+ |---|---|---|
270
+ | `guard.ts` | `tool.execute.before` | Blocks destructive commands in code. Fail-closed. |
271
+ | `prune.ts` | `experimental.chat.messages.transform` | Replaces old tool output with a one-line stub. |
272
+ | `router.ts` | `config`, `event`, `chat.message` | Moves to the next profile when a route is spent. |
273
+ | `telemetry.ts` | `event`, `tool.execute.after` | Local JSONL, gitignored, never a network call. |
274
+
275
+ `guard.ts` matches against the command a shell would actually run, not the text
276
+ it was handed. It splits on shell separators, honours quotes so that a `;`
277
+ inside a string is an argument rather than a second command, strips `env`,
278
+ `sudo` and `git -C` before matching, and follows `sh -c "..."` into its payload.
279
+ So `cd repo && rm -rf /`, `sudo rm -rf /` and `bash -c "rm -rf /"` are all
280
+ caught, while `git log --grep push` and `echo push` are not. `git push` is
281
+ not guard-blocked; it falls through to the `*` ask permission so opencode
282
+ prompts for human approval before publishing.
283
+ Writes are scoped to the project directory, with the OS temp directory as the
284
+ one exception GLOBAL.md allows.
285
+
286
+ Pruning keeps user messages, assistant prose, diffs, and the last few turns
287
+ verbatim, and never touches the in-flight turn. Old reasoning is dropped, since
288
+ stale reasoning is worse than none.
289
+
290
+ ## Install
291
+
292
+ One command if you are new here (`init` asks which free profile to run on,
293
+ then runs the two commands below plus `verify`):
294
+
295
+ ```bash
296
+ npx oc-harness init
297
+ ```
298
+
299
+ Two commands, in this order, if you already know which profile you want
300
+ (`npx oc-harness …` from the registry, or `./bin/oc-harness.mjs …` from a
301
+ checkout — they are the same program):
302
+
303
+ ```bash
304
+ oc-harness use space-bunny-free # writes opencode.jsonc
305
+ oc-harness install # writes everything that file points at
306
+ ```
307
+
308
+ `install` symlinks (not copies) the plugin, `agents/`, `skills/` and `GLOBAL.md`
309
+ into `~/.config/opencode/`, so this repo stays the single source of truth and an
310
+ edit here takes effect on the next opencode start.
311
+
312
+ Then restart opencode. Check it took:
313
+
314
+ ```bash
315
+ opencode debug config | grep -A3 '"agent"'
316
+ opencode agent list | grep -E '^(build|plan|reviewer|locate|verify) '
317
+ ```
318
+
319
+ `oc-harness use` writes the active profile id into the generated config, and
320
+ the plugin records it in the project's `.space/active` at startup so pruning
321
+ picks up the right thresholds. To pin a profile explicitly instead:
322
+
323
+ ```bash
324
+ export OPENCODE_HARNESS_PROFILE=space-bunny-free
325
+ ```
326
+
327
+ ### On your phone
328
+
329
+ **Android (Termux)** runs the whole kit locally — same wizard as desktop:
330
+
331
+ ```bash
332
+ pkg install nodejs git
333
+ npx -y opencode-harness-kit init
334
+ ```
335
+
336
+ Not published to npm yet, or no registry access? The checkout path works
337
+ identically:
338
+
339
+ ```bash
340
+ pkg install nodejs git
341
+ git clone https://github.com/pham-canh/space-harness.git
342
+ ./space-harness/bin/oc-harness.mjs init
343
+ ```
344
+
345
+ The kit writes to `$HOME/.config/opencode` like everywhere else, so there is
346
+ nothing phone-specific to learn. Clone the kit repo too if you want the
347
+ pre-commit hook; without a checkout that step is skipped.
348
+
349
+ **iPhone** cannot run Node locally, so the phone is a remote control, not the
350
+ engine. Run opencode on your Mac or a server (inside `tmux` so it survives a
351
+ dropped connection), then SSH in from the phone — Blink Shell is the usual
352
+ client, Tailscale removes the network setup. The kit, the skills and the
353
+ free-model profiles all live server-side; the phone only needs a keyboard.
354
+
355
+ ### Permissions: why `edit` is `allow`
356
+
357
+ The generated config sets `permission.edit: "allow"`, which looks like it
358
+ contradicts the read-only roles. It does not, and the reason is an ordering rule
359
+ worth knowing before you edit these by hand.
360
+
361
+ OpenCode appends the **global** permission rule *after* the agent's own, and
362
+ **the last matching rule wins**. So a global `edit: "ask"` silently overrides
363
+ every agent's `edit: "deny"` — and the read-only roles can edit files after a
364
+ prompt. `allow` is the permissive *implicit default* and emits no rule at all,
365
+ which leaves each agent's `deny` deciding. Verified with `opencode agent list`:
366
+
367
+ ```
368
+ build edit -> allow
369
+ plan edit -> deny
370
+ locate edit -> deny
371
+ reviewer edit -> deny
372
+ verify edit -> deny
373
+ ```
374
+
375
+ Writes outside the project still ask, via `external_directory`.
376
+
377
+ ## Roles
378
+
379
+ | role | mode | job | effort |
380
+ |---|---|---|---|
381
+ | `build` | primary | writes and verifies code | middle |
382
+ | `plan` | primary | turns a request into an executable plan | highest |
383
+ | `reviewer` | subagent | two passes, never edits | highest |
384
+ | `locate` | subagent | finds code, reports `file:line`, never designs | lowest |
385
+ | `verify` | subagent | runs checks, never fixes them | lowest |
386
+
387
+ `locate` is deliberately *not* called `scout`: OpenCode ships a built-in `scout`
388
+ subagent for dependency and external-docs research, and shipping our own file
389
+ under that name shadowed it with `bash: deny` so it could not do the one thing
390
+ the built-in exists for.
391
+
392
+ Each role carries a `steps` cap. It is a loop breaker, not a quality knob: on
393
+ exhaustion OpenCode forces a text-only response, so an agent that has stopped
394
+ making progress reports rather than looping until the context is gone.
395
+
396
+ ## The skill loop
397
+
398
+ The roles above are the *who*. The skills are the *order*, and they load by
399
+ name from `skills/`:
400
+
401
+ | skill | step | hands to |
402
+ |---|---|---|
403
+ | `space-plan` | plan before code; one task at a time | `space-build` |
404
+ | `space-build` | implement and verify exactly one task | `space-review` |
405
+ | `space-review` | two passes; blockers back to build, a clean pass 1 onward | `space-build` or `space-finish` |
406
+ | `space-finish` | final checks, summary, publish decision | a human |
407
+
408
+ The loop's memory is `.space/tasks.md` in the project being worked on, written
409
+ by the plan step from
410
+ [`skills/space-plan/reference/task-template.md`](skills/space-plan/reference/task-template.md).
411
+ Context is pruned; the file is not. Any step that stops reading it works from
412
+ the conversation instead of the requirement.
413
+
414
+ Publishing is never a loop step. `space-finish` prepares the change; a human
415
+ decides it ships.
416
+
417
+ ## Known limits
418
+
419
+ Read [docs/KNOWN-LIMITS.md](docs/KNOWN-LIMITS.md) before relying on failover.
420
+ The short version: no OpenCode plugin hook can change a live session's model,
421
+ so switching happens at the next session, not mid-flight.
422
+
423
+ ## Development
424
+
425
+ ```bash
426
+ npm install
427
+ npm run check # everything: build, typecheck, tests, live install, verify
428
+ ```
429
+
430
+ Or one layer at a time:
431
+
432
+ ```bash
433
+ npm test # model-agnostic assertions, no network, no model calls
434
+ npm run typecheck
435
+ npm run eval # synthetic: pruning, prompt pressure, effort projection
436
+ npm run measure # is this install behaving? narrow reads, context
437
+ npm run attribute # where the context went: fixed overhead vs history
438
+ npm run thresholds # propose the gate floor from what this install actually did
439
+ npm run baseline # your own install, from scratch
440
+ ```
441
+
442
+ `eval/ab.mjs` runs the same tasks with and without the kit, to separate the kit
443
+ from the work. It spawns real agents, so it is not part of `npm run check`, and
444
+ it wants a subject it can hand each run a private copy of:
445
+
446
+ ```bash
447
+ node eval/prepare.mjs --repo ~/work/lablab-harness --ref 8a674f5
448
+ node eval/ab.mjs --target <printed path> --suite eval/tasks.json --reps 2 --sanitize
449
+ node eval/ab.mjs --rescore <saved stream dir> # re-grade without spawning agents
450
+ ```
451
+
452
+ `npm test` and `npm run typecheck` need nothing but the repo, so CI runs those.
453
+ The rest read `~/.local/share/opencode/opencode.db` and `~/.config/opencode`,
454
+ which only exist on a machine that has run OpenCode with the kit installed -
455
+ `npm run check:online` additionally hits the model catalog over the network.
456
+
457
+ Measured results, and the limits they do not cross, are in
458
+ [eval/RESULTS.md](eval/RESULTS.md).
459
+
460
+ `docs/RESEARCH.md` records where the numbers came from and what was tried and
461
+ rejected.
462
+
463
+ ## License
464
+
465
+ MIT
@@ -0,0 +1,66 @@
1
+ ---
2
+ description: Writes code and verifies it. Use when the plan is settled and files need to change.
3
+ mode: primary
4
+ model: opencode/space-bunny-free
5
+ variant: high
6
+ steps: 40
7
+ permission:
8
+ edit: allow
9
+ bash:
10
+ "*": ask
11
+ "git status *": allow
12
+ "git log *": allow
13
+ "git diff *": allow
14
+ "git add*": allow
15
+ "git -C * status*": allow
16
+ "git -C * log*": allow
17
+ "git -C * diff*": allow
18
+ "grep*": allow
19
+ "rg*": allow
20
+ "find*": allow
21
+ "cat*": allow
22
+ "ls*": allow
23
+ "mkdir*": allow
24
+ "chmod +x*": allow
25
+ "python3 -m pytest*": allow
26
+ "pytest*": allow
27
+ "ruff*": allow
28
+ "mypy*": allow
29
+ "npm test*": allow
30
+ "npm run *": allow
31
+ "npx tsc*": allow
32
+ ---
33
+
34
+ You implement one task from a plan. One. Not the whole plan.
35
+
36
+ ## Loop
37
+
38
+ 1. Read the plan task. Read the files it names — the whole file when you are
39
+ about to edit it, and a narrow range when you are only orienting.
40
+ 2. Write the smallest change that satisfies it.
41
+ 3. Run the verify command the plan gave you.
42
+ 4. If it fails, read the actual error, fix the cause, run it again.
43
+ 5. If it passes, report the command and its real output.
44
+
45
+ ## Rules
46
+
47
+ - The plan's verify command is the definition of done. If there is none, run the
48
+ project's default test command and say that is what you used.
49
+ - Never leave a stub, a `TODO`, or a comment promising to finish later. If you
50
+ cannot complete the task, say so before writing a partial one.
51
+ - Never widen the change. A drive-by refactor in the same file is a separate
52
+ task; do not report it as part of this one.
53
+ - Ask before `git push`, publish, or opening a pull request. Committing is
54
+ allowed but only when the task asked for it.
55
+ - Never fabricate a passing result. A failing test reported honestly is worth
56
+ more than a green one you did not get.
57
+ - Never re-run a command just to get its output back into context. Say what you
58
+ need from it and fetch only that.
59
+
60
+ ## Report
61
+
62
+ Which task you completed, the command you ran, and its verbatim final lines. If
63
+ anything is unfinished, list it explicitly.
64
+
65
+ See also the `space-build` skill for the full plan → build → review → finish
66
+ loop and the shared task-file contract.