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.
- package/GLOBAL.md +110 -0
- package/LICENSE +21 -0
- package/README.md +464 -2
- package/agents/build.md +66 -0
- package/agents/locate.md +48 -0
- package/agents/plan.md +61 -0
- package/agents/reviewer.md +62 -0
- package/agents/verify.md +54 -0
- package/bin/baseline.mjs +367 -0
- package/bin/git-hooks/pre-commit +30 -0
- package/bin/oc-harness.mjs +1025 -0
- package/dist/index.js +1038 -0
- package/docs/KNOWN-LIMITS.md +188 -0
- package/docs/RESEARCH.md +210 -0
- package/docs/demo/demo.gif +0 -0
- package/docs/demo/demo.mp4 +0 -0
- package/docs/demo/render.py +146 -0
- package/examples/muse-spark-1.3-contributor-free.json +41 -0
- package/package.json +81 -4
- package/profiles/deepseek-v4-flash-free.json +41 -0
- package/profiles/index.json +78 -0
- package/profiles/longcat-2.5-preview-free.json +41 -0
- package/profiles/muse-split-free.json +64 -0
- package/profiles/schema.json +161 -0
- package/profiles/space-bunny-free.json +42 -0
- package/profiles/x-preview-f-free.json +41 -0
- package/retention.yml +110 -0
- package/skills/space-build/SKILL.md +54 -0
- package/skills/space-finish/SKILL.md +63 -0
- package/skills/space-plan/SKILL.md +101 -0
- package/skills/space-plan/reference/task-template.md +47 -0
- package/skills/space-review/SKILL.md +53 -0
- package/src/config.js +358 -0
- package/src/effort.js +98 -0
- package/src/guard.ts +59 -0
- package/src/index.ts +80 -0
- package/src/profile.js +484 -0
- package/src/prune.ts +258 -0
- package/src/router.ts +192 -0
- package/src/rules.js +481 -0
- 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
|
-
#
|
|
1
|
+
# opencode-harness-kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/pham-canh/space-harness/actions/workflows/ci.yml)
|
|
4
|
+
[](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
|
+

|
|
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
|
package/agents/build.md
ADDED
|
@@ -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.
|