opencode-harness-kit 0.4.3 → 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 +527 -18
  2. package/CONTRIBUTING.md +242 -0
  3. package/GLOBAL.md +34 -5
  4. package/MIGRATION.md +151 -0
  5. package/README.md +307 -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
@@ -43,6 +43,10 @@
43
43
  "sweep": "none",
44
44
  "longform": null
45
45
  },
46
+ "utility": {
47
+ "model": "deepseek-v4.1-flash-free",
48
+ "why": "Cheapest route measured for a short auxiliary call: 49 reasoning tokens and 7.2s for a title, against 110 and 13.4s on big-pickle and 332 and 10.6s on nemotron-3.5-super-vl. It is in seven of this fleet's ten chains, so a failover has a tier-capable model to fall back to."
49
+ },
46
50
  "roleModels": {
47
51
  "build": {
48
52
  "model": "minimax-m3.1-flash-free",
@@ -73,7 +77,8 @@
73
77
  "chain": [
74
78
  "nemotron-3.5-lightning-free",
75
79
  "big-pickle",
76
- "longcat-2.5-preview-free"
80
+ "longcat-2.5-preview-free",
81
+ "deepseek-v4.1-flash-free"
77
82
  ]
78
83
  },
79
84
  "verify": {
@@ -40,9 +40,13 @@
40
40
  "vision": null,
41
41
  "doc": null,
42
42
  "schema": "max",
43
- "sweep": "none",
43
+ "sweep": "low",
44
44
  "longform": null
45
45
  },
46
+ "utility": {
47
+ "model": "deepseek-v4.1-flash-free",
48
+ "why": "Cheapest route measured for a short auxiliary call: 49 reasoning tokens and 7.2s for a title, against 110 and 13.4s on big-pickle and 332 and 10.6s on nemotron-3.5-super-vl. It is also in every chain of this fleet, so a failover cannot leave the utility route pointing at a model that is not in the fleet."
49
+ },
46
50
  "roleModels": {
47
51
  "build": {
48
52
  "model": "minimax-m3.1-flash-free",
@@ -109,10 +113,10 @@
109
113
  ]
110
114
  },
111
115
  "sweep": {
112
- "model": "longcat-2.5-preview-free",
116
+ "model": "deepseek-v4.1-flash-free",
113
117
  "chain": [
114
- "longcat-2.5-preview-free",
115
118
  "deepseek-v4.1-flash-free",
119
+ "longcat-2.5-preview-free",
116
120
  "minimax-m3.1-flash-free"
117
121
  ]
118
122
  },
@@ -1,17 +1,31 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://github.com/anomalyco/opencode-harness-kit/profiles/schema.json",
3
+ "$id": "https://github.com/pham-canh/space-harness/profiles/schema.json",
4
4
  "title": "OpenCode harness profile",
5
5
  "description": "Everything that changes when you change model. Plugins read this file and contain no model names of their own.",
6
6
  "type": "object",
7
- "required": ["id", "provider", "model", "retention", "limits", "effort", "roles"],
7
+ "required": [
8
+ "id",
9
+ "provider",
10
+ "model",
11
+ "retention",
12
+ "limits",
13
+ "effort",
14
+ "roles"
15
+ ],
8
16
  "additionalProperties": false,
9
17
  "properties": {
18
+ "$comment": {
19
+ "type": "string",
20
+ "description": "Free text for a human. Allowed here because every shipped profile carries one, and lanes.schema.json declares it too."
21
+ },
10
22
  "id": {
11
23
  "type": "string",
12
24
  "description": "Slug used as the profile filename and the oc-harness use argument."
13
25
  },
14
- "title": { "type": "string" },
26
+ "title": {
27
+ "type": "string"
28
+ },
15
29
  "provider": {
16
30
  "type": "string",
17
31
  "description": "opencode provider id, e.g. opencode, opencode-go, anthropic."
@@ -21,7 +35,10 @@
21
35
  "description": "Model id without the provider prefix, e.g. beam-free."
22
36
  },
23
37
  "family": {
24
- "type": ["string", "null"],
38
+ "type": [
39
+ "string",
40
+ "null"
41
+ ],
25
42
  "description": "Model family for grouping. Stealth models report null upstream, so this may be set by hand."
26
43
  },
27
44
  "verified": {
@@ -29,23 +46,46 @@
29
46
  "description": "ISO date a human checked this profile against live metadata."
30
47
  },
31
48
  "retention": {
32
- "enum": ["zero-retention", "retained", "may-train", "unknown"],
49
+ "enum": [
50
+ "zero-retention",
51
+ "retained",
52
+ "may-train",
53
+ "unknown"
54
+ ],
33
55
  "description": "Must match an entry in retention.yml. A profile whose model is unknown there cannot become the default."
34
56
  },
35
57
  "maxRetention": {
36
- "enum": ["zero-retention", "retained", "may-train"],
58
+ "enum": [
59
+ "zero-retention",
60
+ "retained",
61
+ "may-train"
62
+ ],
37
63
  "description": "The worst retention a lane in this profile is allowed to resolve to. A lane walks its model chain and takes the first rung at or below this ceiling, so one profile can serve a fleet without sending a prompt to a provider that trains on it. Defaults to the profile's own retention."
38
64
  },
39
- "notes": { "type": "string" },
65
+ "notes": {
66
+ "type": "string"
67
+ },
40
68
  "limits": {
41
69
  "type": "object",
42
- "required": ["context", "output"],
70
+ "required": [
71
+ "context",
72
+ "output"
73
+ ],
43
74
  "additionalProperties": false,
44
75
  "description": "Token ceilings from the provider catalog. `input` is absent for several free models, in which case the loader derives it from `context`.",
45
76
  "properties": {
46
- "context": { "type": "integer", "minimum": 1 },
47
- "input": { "type": "integer", "minimum": 1 },
48
- "output": { "type": "integer", "minimum": 1 }
77
+ "context": {
78
+ "type": "integer",
79
+ "minimum": 1
80
+ },
81
+ "input": {
82
+ "type": "integer",
83
+ "minimum": 1
84
+ },
85
+ "output": {
86
+ "type": "integer",
87
+ "minimum": 1
88
+ }
49
89
  }
50
90
  },
51
91
  "supports": {
@@ -53,10 +93,18 @@
53
93
  "additionalProperties": false,
54
94
  "description": "Capability flags. Missing or false means do not emit the related config.",
55
95
  "properties": {
56
- "reasoning": { "type": "boolean" },
57
- "temperature": { "type": "boolean" },
58
- "toolCall": { "type": "boolean" },
59
- "structuredOutput": { "type": "boolean" },
96
+ "reasoning": {
97
+ "type": "boolean"
98
+ },
99
+ "temperature": {
100
+ "type": "boolean"
101
+ },
102
+ "toolCall": {
103
+ "type": "boolean"
104
+ },
105
+ "structuredOutput": {
106
+ "type": "boolean"
107
+ },
60
108
  "attachment": {
61
109
  "type": "boolean",
62
110
  "description": "Accepts image or video input. Gates screenshot-based verification."
@@ -69,10 +117,26 @@
69
117
  "properties": {
70
118
  "levels": {
71
119
  "type": "array",
72
- "items": { "type": "string" },
73
- "examples": [["low", "medium", "high", "xhigh", "max"]]
120
+ "items": {
121
+ "type": "string"
122
+ },
123
+ "examples": [
124
+ [
125
+ "low",
126
+ "medium",
127
+ "high",
128
+ "xhigh",
129
+ "max"
130
+ ]
131
+ ]
132
+ },
133
+ "default": {
134
+ "type": [
135
+ "string",
136
+ "null"
137
+ ],
138
+ "description": "The level the session runs at by default. Null means the model has no tunable effort and no variant is emitted."
74
139
  },
75
- "default": { "type": "string" },
76
140
  "reserveShare": {
77
141
  "type": "number",
78
142
  "minimum": 0,
@@ -84,19 +148,57 @@
84
148
  },
85
149
  "roles": {
86
150
  "type": "object",
87
- "description": "Requested effort per lane. Resolved against effort.levels, never emitted raw. A lane listed here is enabled; a lane absent from here is not installed.",
151
+ "description": "Requested effort per lane, never emitted raw. A lane listed here is enabled; a lane absent from here is not installed. It is resolved against the levels of the model the lane actually runs on, which is the profile's own effort.levels for a single-model profile and the lane model's levels for a fleet. `none` means reasoning off: a model that does not publish it gets no variant at all.",
88
152
  "additionalProperties": false,
89
153
  "properties": {
90
- "build": { "$ref": "#/definitions/effortRequest" },
91
- "plan": { "$ref": "#/definitions/effortRequest" },
92
- "reviewer": { "$ref": "#/definitions/effortRequest" },
93
- "locate": { "$ref": "#/definitions/effortRequest" },
94
- "verify": { "$ref": "#/definitions/effortRequest" },
95
- "vision": { "$ref": "#/definitions/effortRequest" },
96
- "doc": { "$ref": "#/definitions/effortRequest" },
97
- "schema": { "$ref": "#/definitions/effortRequest" },
98
- "sweep": { "$ref": "#/definitions/effortRequest" },
99
- "longform": { "$ref": "#/definitions/effortRequest" }
154
+ "build": {
155
+ "$ref": "#/definitions/effortRequest"
156
+ },
157
+ "plan": {
158
+ "$ref": "#/definitions/effortRequest"
159
+ },
160
+ "reviewer": {
161
+ "$ref": "#/definitions/effortRequest"
162
+ },
163
+ "locate": {
164
+ "$ref": "#/definitions/effortRequest"
165
+ },
166
+ "verify": {
167
+ "$ref": "#/definitions/effortRequest"
168
+ },
169
+ "vision": {
170
+ "$ref": "#/definitions/effortRequest"
171
+ },
172
+ "doc": {
173
+ "$ref": "#/definitions/effortRequest"
174
+ },
175
+ "schema": {
176
+ "$ref": "#/definitions/effortRequest"
177
+ },
178
+ "sweep": {
179
+ "$ref": "#/definitions/effortRequest"
180
+ },
181
+ "longform": {
182
+ "$ref": "#/definitions/effortRequest"
183
+ }
184
+ }
185
+ },
186
+ "utility": {
187
+ "type": "object",
188
+ "description": "The route opencode uses for its short auxiliary calls - a title, a one-line summary. Optional: a profile without one keeps the session model, which is what a single-model profile wants, because 'one route, and it is the one you asked for' is a promise those profiles make.",
189
+ "additionalProperties": false,
190
+ "required": [
191
+ "model"
192
+ ],
193
+ "properties": {
194
+ "model": {
195
+ "type": "string",
196
+ "description": "Model id without the provider prefix. Must be a model this profile already resolves to, so the utility route is never a model the fleet cannot run. The route itself does not change on failover; the profile keeps it."
197
+ },
198
+ "why": {
199
+ "type": "string",
200
+ "description": "The measurement that chose it. A route chosen by taste is a route that drifts."
201
+ }
100
202
  }
101
203
  },
102
204
  "roleModels": {
@@ -104,16 +206,36 @@
104
206
  "description": "Model per lane. A lane absent from `roles` is not enabled. A lane absent from `roleModels` runs on the profile's base model.",
105
207
  "additionalProperties": false,
106
208
  "properties": {
107
- "build": { "$ref": "#/definitions/roleModel" },
108
- "plan": { "$ref": "#/definitions/roleModel" },
109
- "reviewer": { "$ref": "#/definitions/roleModel" },
110
- "locate": { "$ref": "#/definitions/roleModel" },
111
- "verify": { "$ref": "#/definitions/roleModel" },
112
- "vision": { "$ref": "#/definitions/roleModel" },
113
- "doc": { "$ref": "#/definitions/roleModel" },
114
- "schema": { "$ref": "#/definitions/roleModel" },
115
- "sweep": { "$ref": "#/definitions/roleModel" },
116
- "longform": { "$ref": "#/definitions/roleModel" }
209
+ "build": {
210
+ "$ref": "#/definitions/roleModel"
211
+ },
212
+ "plan": {
213
+ "$ref": "#/definitions/roleModel"
214
+ },
215
+ "reviewer": {
216
+ "$ref": "#/definitions/roleModel"
217
+ },
218
+ "locate": {
219
+ "$ref": "#/definitions/roleModel"
220
+ },
221
+ "verify": {
222
+ "$ref": "#/definitions/roleModel"
223
+ },
224
+ "vision": {
225
+ "$ref": "#/definitions/roleModel"
226
+ },
227
+ "doc": {
228
+ "$ref": "#/definitions/roleModel"
229
+ },
230
+ "schema": {
231
+ "$ref": "#/definitions/roleModel"
232
+ },
233
+ "sweep": {
234
+ "$ref": "#/definitions/roleModel"
235
+ },
236
+ "longform": {
237
+ "$ref": "#/definitions/roleModel"
238
+ }
117
239
  }
118
240
  },
119
241
  "prune": {
@@ -121,7 +243,9 @@
121
243
  "additionalProperties": false,
122
244
  "description": "Deterministic context pruning. Only used when a model-driven compressor is unavailable or unproven.",
123
245
  "properties": {
124
- "enabled": { "type": "boolean" },
246
+ "enabled": {
247
+ "type": "boolean"
248
+ },
125
249
  "keepRecentToolCalls": {
126
250
  "type": "integer",
127
251
  "minimum": 1,
@@ -137,14 +261,16 @@
137
261
  "keepUserMessages": {
138
262
  "type": "boolean",
139
263
  "default": true,
140
- "description": "Never stub a user message. Their instructions are the point of the session."
264
+ "description": "Never stub anything inside a user message. Their instructions are the point of the session. This covers a tool result a user attached as well as their text: user text is kept either way, because prose is kept everywhere."
141
265
  }
142
266
  }
143
267
  },
144
268
  "fallback": {
145
269
  "type": "array",
146
270
  "description": "Ordered profile ids to fall back to. Selection happens at session start, not mid-session: no opencode plugin hook can change a live session's model.",
147
- "items": { "type": "string" }
271
+ "items": {
272
+ "type": "string"
273
+ }
148
274
  }
149
275
  },
150
276
  "definitions": {
@@ -152,28 +278,33 @@
152
278
  "type": "object",
153
279
  "description": "A model for one lane, plus the effort levels that model actually offers, plus the ordered list of models that may serve the lane when the first one is spent or too private.",
154
280
  "additionalProperties": false,
155
- "required": ["model"],
281
+ "required": [
282
+ "model"
283
+ ],
156
284
  "properties": {
157
285
  "model": {
158
286
  "type": "string",
159
287
  "description": "Model id without the provider prefix. Must resolve to a profile in this directory, so its limits and retention are known."
160
288
  },
161
- "levels": {
162
- "type": "array",
163
- "items": { "type": "string" },
164
- "description": "Effort levels this role's model offers. Omitted means the role emits no variant at all, which is the right reading for a model with no tunable effort."
165
- },
166
289
  "chain": {
167
290
  "type": "array",
168
- "items": { "type": "string" },
291
+ "items": {
292
+ "type": "string"
293
+ },
169
294
  "description": "Ordered models that may serve this lane, best first. The first rung the profile's maxRetention ceiling allows is the one that runs it, and the router walks the same chain when a route is spent."
170
295
  }
171
296
  }
172
297
  },
173
298
  "effortRequest": {
174
299
  "oneOf": [
175
- { "type": "string", "description": "Desired level, resolved to the nearest available one." },
176
- { "type": "null", "description": "This role should not set a variant at all." }
300
+ {
301
+ "type": "string",
302
+ "description": "Desired level, resolved to the nearest available one."
303
+ },
304
+ {
305
+ "type": "null",
306
+ "description": "This role should not set a variant at all."
307
+ }
177
308
  ]
178
309
  }
179
310
  }
package/prompts/build.md CHANGED
@@ -17,8 +17,10 @@ You implement one task from a plan. One. Not the whole plan.
17
17
  cannot complete the task, say so before writing a partial one.
18
18
  - Never widen the change. A drive-by refactor in the same file is a separate
19
19
  task; do not report it as part of this one.
20
- - Ask before `git push`, publish, or opening a pull request. Committing is
21
- allowed but only when the task asked for it.
20
+ - Committing and pushing the branch you were asked to finish is allowed, and
21
+ you say what you pushed. Ask before publishing a package, opening a pull
22
+ request, or changing repository visibility. Committing is allowed but only
23
+ when the task asked for it.
22
24
  - Never fabricate a passing result. A failing test reported honestly is worth
23
25
  more than a green one you did not get.
24
26
  - Never re-run a command just to get its output back into context. Say what you
@@ -25,8 +25,10 @@ description: Implement one planned task and verify it with a real command. Use w
25
25
  Fix the cause, then run it again. The third identical attempt is a signal to
26
26
  stop and report.
27
27
  7. **Mark the task done in `.space/tasks.md`** and paste the verify command's
28
- verbatim final lines into its `verify:` line. A task marked done without
29
- that has not been verified.
28
+ verbatim final lines into its `output:` line. The `verify:` line keeps the
29
+ command and nothing else - it is what the driver runs, so a `verify:` that
30
+ reads `npm test - 3 passed` is a command that does not exist. A task marked
31
+ done without the output has not been verified.
30
32
  8. **Report the command and its verbatim output.**
31
33
 
32
34
  ## Test-first when the plan called for it
@@ -37,8 +37,12 @@ over an unreviewed or blocked change is the failure this skill exists to stop.
37
37
 
38
38
  ## The publish boundary
39
39
 
40
- Publishing needs human approval. Ask before `git push`, opening a pull request,
41
- publishing a package, or changing repository visibility, even if the task
40
+ Pushing the branch you were asked to finish is not one of the decisions that
41
+ need a human: commit it, push it, and say what you pushed.
42
+
43
+ Everything outward-facing past the push is. Ask before publishing a package,
44
+ opening a pull request, changing repository visibility, force-pushing,
45
+ rewriting published history, or deleting a branch or tag - even if the task
42
46
  seems to imply it. Permissions default to ask so opencode prompts first, but
43
47
  the boundary is yours to hold, not only the hook's.
44
48
 
@@ -20,15 +20,23 @@ has been pruned. Never keep it in your head.
20
20
  - [ ] <task, small enough to finish in a few minutes>
21
21
  - files: <exact paths to change>
22
22
  - change: <what the change is, concretely, not "handle errors">
23
- - verify: <the exact command, and what passing looks like>
24
- - status: pending | done | blocked
23
+ - verify: <the exact command that proves it, and nothing else>
24
+ - status: <pending | done | blocked>
25
+ - output: <the command's verbatim final lines>
25
26
  - [x] <finished task>
26
27
  - files: <paths>
27
28
  - change: <what was done, including anything the original task did not name>
28
- - verify: <the command that was actually run, and its verbatim final lines>
29
+ - verify: <the command that was actually run>
30
+ - output: <the command's verbatim final lines>
29
31
  - status: done
30
32
  ```
31
33
 
34
+ `verify:` is a command and nothing else, because the driver runs it. The
35
+ output of that command goes on the `output:` line under it, never on the
36
+ `verify:` line and never appended to it: a `verify:` that reads
37
+ `npm test - 3 passed` is a command that does not exist, and the loop would
38
+ fail on the command rather than on the work.
39
+
32
40
  How the loop uses it:
33
41
 
34
42
  | step | does |
@@ -41,7 +49,7 @@ How the loop uses it:
41
49
  Rules:
42
50
 
43
51
  - One file per body of work. Start a new one only when the goal changes.
44
- - A task is `done` only with the verify command's real output pasted into its
45
- `verify:` line. A task marked done without it has not been verified.
52
+ - A task is `done` only with the verify command's real output in its `output:`
53
+ line. A task marked done without it has not been verified.
46
54
  - Add a Decisions line the moment something was chosen rather than asked. That
47
55
  is the line that stops the next session re-litigating it.