harness-everything 0.47.2 → 0.47.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/ci.yml +2 -0
- package/CHANGELOG.md +16 -0
- package/behavioral-evals/README.md +10 -0
- package/behavioral-evals/case-validator.js +7 -0
- package/behavioral-evals/cases/baseline-profile-lookup-json-only.yaml +23 -0
- package/behavioral-evals/run.js +85 -6
- package/benchmarks/issue290-pilot/metrics.test.py +60 -2
- package/ci/lib/opencode-verification-loader.js +24 -0
- package/ci/mechanism-2p-opencode-v2-entrypoint.test.js +8 -1
- package/ci/mechanism-325-f3-derived-views.test.js +42 -2
- package/ci/mechanism-338-opencode-async.test.js +229 -0
- package/ci/mechanism-341-opencode-lane.test.js +14 -3
- package/ci/mechanism-36-verify-timeout.test.js +7 -168
- package/ci/test-behavioral-case-validation.js +148 -3
- package/docs/platform-capabilities.md +8 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-check.js +5 -4
- package/hooks/scripts/lib/doc-reconciliation-provider.js +1 -1
- package/opencode-plugin/README.md +28 -30
- package/opencode-plugin/index.mjs +249 -112
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-check.js +5 -4
- package/sbom.cdx.json +7 -7
|
@@ -5,8 +5,8 @@ const fs = require('fs');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const { parseSimpleYaml } = require('../behavioral-evals/run');
|
|
8
|
-
const { grade, parseTranscriptEvents } = require('../behavioral-evals/run');
|
|
9
|
-
const { validateCase, validateDirectory, negativeControls } = require('../behavioral-evals/case-validator');
|
|
8
|
+
const { grade, parseTranscriptEvents, checkJsonOnlyOutput, finalAssistantText } = require('../behavioral-evals/run');
|
|
9
|
+
const { validateCase, validateDirectory, validateExpectation, negativeControls } = require('../behavioral-evals/case-validator');
|
|
10
10
|
const { canonicalRubric, extractTrace } = require('./ab-test-harness');
|
|
11
11
|
|
|
12
12
|
const casesDir = path.join(__dirname, '..', 'behavioral-evals', 'cases');
|
|
@@ -85,4 +85,149 @@ try {
|
|
|
85
85
|
fs.rmSync(transcriptDir, { recursive: true, force: true });
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
// final_output_json_only: pure checker, validator schema, shipped #308 case, and
|
|
89
|
+
// end-to-end grading over the final assistant message.
|
|
90
|
+
const SELECTION_KEYS = ['requestedProfile', 'effectiveProfile', 'profileAlias', 'assignedRole', 'runtimeModel', 'runtimeEffort'];
|
|
91
|
+
const SELECTION = { requestedProfile: 'fable', effectiveProfile: 'sonnet', profileAlias: 'fable-on-sonnet', assignedRole: 'worker', runtimeModel: 'claude-haiku-5-5', runtimeEffort: 'low' };
|
|
92
|
+
const LABEL = '<!-- harness-label {"judgement":"done","routing":"direct"} -->';
|
|
93
|
+
const RAW = JSON.stringify(SELECTION);
|
|
94
|
+
const FENCED = '```json\n' + JSON.stringify(SELECTION, null, 2) + '\n```';
|
|
95
|
+
const PASS_CASES = [
|
|
96
|
+
['raw JSON object', RAW],
|
|
97
|
+
['fenced json block', FENCED],
|
|
98
|
+
['untagged fence', '```\n' + RAW + '\n```'],
|
|
99
|
+
['raw JSON followed by harness-label line', RAW + '\n' + LABEL],
|
|
100
|
+
['fenced JSON followed by harness-label line', FENCED + '\n' + LABEL],
|
|
101
|
+
];
|
|
102
|
+
for (const [name, text] of PASS_CASES) {
|
|
103
|
+
const verdict = checkJsonOnlyOutput(text, SELECTION_KEYS);
|
|
104
|
+
assert.strictEqual(verdict.pass, true, `${name} should pass: ${verdict.reason}`);
|
|
105
|
+
}
|
|
106
|
+
const FAIL_CASES = [
|
|
107
|
+
// The #308 pilot: correct fields, explanatory prose outside the JSON.
|
|
108
|
+
['prose before JSON (#308)', `"Fable on sonnect" means the fable profile runs on sonnet.\n\n${FENCED}`, /prose before/],
|
|
109
|
+
['prose after fence', `${FENCED}\nThat is the selection record.`, /outside the single JSON code fence/],
|
|
110
|
+
['prose after raw JSON', `${RAW}\nThat is the selection record.`, /not a single valid JSON value/],
|
|
111
|
+
['two fenced blocks', `${FENCED}\n${FENCED}`, /more than one code fence/],
|
|
112
|
+
['missing required key', JSON.stringify({ ...SELECTION, runtimeEffort: undefined }), /missing required JSON keys: runtimeEffort/],
|
|
113
|
+
['non-object JSON (array)', JSON.stringify([SELECTION]), /must be an object/],
|
|
114
|
+
['non-object JSON (scalar)', '"fable"', /must be an object/],
|
|
115
|
+
['empty final text', '', /empty/],
|
|
116
|
+
['label-only final text', LABEL, /empty/],
|
|
117
|
+
['missing final text', undefined, /missing/],
|
|
118
|
+
];
|
|
119
|
+
for (const [name, text, expected] of FAIL_CASES) {
|
|
120
|
+
const verdict = checkJsonOnlyOutput(text, SELECTION_KEYS);
|
|
121
|
+
assert.strictEqual(verdict.pass, false, `${name} should fail`);
|
|
122
|
+
assert.match(verdict.reason, expected, `${name} should give a clear reason`);
|
|
123
|
+
}
|
|
124
|
+
assert.strictEqual(checkJsonOnlyOutput(RAW).pass, true, 'required_keys defaults to no key requirement');
|
|
125
|
+
|
|
126
|
+
for (const requiredKeys of ['requestedProfile', [''], [' '], [1], ['ok', null]]) {
|
|
127
|
+
const errors = validateExpectation({ type: 'final_output_json_only', required_keys: requiredKeys }, 0);
|
|
128
|
+
assert.ok(errors.some(error => /required_keys must be an array of non-empty strings/.test(error)), `required_keys ${JSON.stringify(requiredKeys)} should be rejected`);
|
|
129
|
+
}
|
|
130
|
+
assert.deepStrictEqual(validateExpectation({ type: 'final_output_json_only' }, 0), [], 'final_output_json_only without required_keys is valid');
|
|
131
|
+
assert.deepStrictEqual(validateExpectation({ type: 'final_output_json_only', required_keys: SELECTION_KEYS }, 0), [], 'final_output_json_only with string keys is valid');
|
|
132
|
+
|
|
133
|
+
const profileCasePath = path.join(casesDir, 'baseline-profile-lookup-json-only.yaml');
|
|
134
|
+
const profileCase = parseSimpleYaml(fs.readFileSync(profileCasePath, 'utf8'));
|
|
135
|
+
assert.deepStrictEqual(validateCase(profileCase, { fileName: 'baseline-profile-lookup-json-only.yaml' }), [], 'profile-lookup JSON-only case should validate');
|
|
136
|
+
assert.deepStrictEqual(profileCase.expectations.map(expectation => expectation.type), ['final_output_json_only'], 'profile-lookup case should assert JSON-only output');
|
|
137
|
+
assert.deepStrictEqual(profileCase.expectations[0].required_keys, SELECTION_KEYS, 'profile-lookup case should require the six selection keys');
|
|
138
|
+
assert.match(profileCase.prompt, /return only a small JSON selection record with requestedProfile, effectiveProfile, profileAlias, assignedRole, runtimeModel, runtimeEffort\. No orchestration or delegation\./, 'profile-lookup prompt should match the #308 wording');
|
|
139
|
+
|
|
140
|
+
const jsonOnlyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'behavioral-json-only-'));
|
|
141
|
+
try {
|
|
142
|
+
const writeTranscript = (name, finalText) => {
|
|
143
|
+
const transcriptPath = path.join(jsonOnlyDir, name);
|
|
144
|
+
const events = [
|
|
145
|
+
{ type: 'tool_use', part: { type: 'tool', tool: 'Read', callID: 'read-1', state: { status: 'completed', input: { file_path: 'README.md' } } } },
|
|
146
|
+
{ type: 'text', part: { type: 'text', text: 'Checking the profile table first.' } },
|
|
147
|
+
{ type: 'tool_use', part: { type: 'tool', tool: 'Read', callID: 'read-2', state: { status: 'completed', input: { file_path: 'profiles.md' } } } },
|
|
148
|
+
{ type: 'text', part: { type: 'text', text: finalText } },
|
|
149
|
+
];
|
|
150
|
+
fs.writeFileSync(transcriptPath, events.map(event => JSON.stringify(event)).join('\n'));
|
|
151
|
+
return transcriptPath;
|
|
152
|
+
};
|
|
153
|
+
const jsonOnlyExpectation = { expectations: [{ type: 'final_output_json_only', required_keys: SELECTION_KEYS }] };
|
|
154
|
+
let transcriptCount = 0;
|
|
155
|
+
const gradeFinal = finalText => grade(jsonOnlyExpectation, jsonOnlyDir, writeTranscript(`final-${transcriptCount++}.jsonl`, finalText));
|
|
156
|
+
assert.strictEqual(gradeFinal(RAW).passed, true, 'raw JSON final message should pass end to end');
|
|
157
|
+
assert.strictEqual(gradeFinal(`${FENCED}\n${LABEL}`).passed, true, 'fenced JSON with harness-label should pass end to end');
|
|
158
|
+
const prose = gradeFinal(`Fable on sonnect means a profile.\n${FENCED}`);
|
|
159
|
+
assert.strictEqual(prose.passed, false, 'prose before JSON must fail end to end');
|
|
160
|
+
assert.strictEqual(prose.results[0].status, 'fail');
|
|
161
|
+
assert.match(prose.results[0].reason, /prose before/);
|
|
162
|
+
// Prose from an earlier turn (before the last tool call) is not the final message.
|
|
163
|
+
const earlierProse = writeTranscript('earlier-prose.jsonl', RAW);
|
|
164
|
+
assert.strictEqual(grade(jsonOnlyExpectation, jsonOnlyDir, earlierProse).passed, true, 'only the final assistant message is graded');
|
|
165
|
+
|
|
166
|
+
// Multi-part final messages: the final turn may stream several text parts.
|
|
167
|
+
const writeParts = (name, finalParts) => {
|
|
168
|
+
const transcriptPath = path.join(jsonOnlyDir, name);
|
|
169
|
+
const events = [
|
|
170
|
+
{ type: 'tool_use', part: { type: 'tool', tool: 'Read', callID: 'read-1', state: { status: 'completed', input: { file_path: 'profiles.md' } } } },
|
|
171
|
+
...finalParts.map(text => ({ type: 'text', part: { type: 'text', text } })),
|
|
172
|
+
];
|
|
173
|
+
fs.writeFileSync(transcriptPath, events.map(event => JSON.stringify(event)).join('\n'));
|
|
174
|
+
return transcriptPath;
|
|
175
|
+
};
|
|
176
|
+
const proseThenJson = grade(jsonOnlyExpectation, jsonOnlyDir, writeParts('prose-part-json-part.jsonl', ['"Fable on sonnect" means the fable profile on sonnet.', FENCED]));
|
|
177
|
+
assert.strictEqual(proseThenJson.passed, false, 'prose part + JSON part must fail (#308 false pass)');
|
|
178
|
+
assert.match(proseThenJson.results[0].reason, /prose before/);
|
|
179
|
+
const jsonThenProse = grade(jsonOnlyExpectation, jsonOnlyDir, writeParts('json-part-prose-part.jsonl', [FENCED, 'That is the selection record.']));
|
|
180
|
+
assert.strictEqual(jsonThenProse.passed, false, 'JSON part + prose part must fail');
|
|
181
|
+
const splitBody = JSON.stringify(SELECTION, null, 2);
|
|
182
|
+
const cut = splitBody.indexOf('"assignedRole"');
|
|
183
|
+
const splitJson = grade(jsonOnlyExpectation, jsonOnlyDir, writeParts('split-json.jsonl', ['```json\n' + splitBody.slice(0, cut), splitBody.slice(cut) + '\n```']));
|
|
184
|
+
assert.strictEqual(splitJson.passed, true, `JSON split across text parts should pass: ${splitJson.results[0].reason}`);
|
|
185
|
+
|
|
186
|
+
// Transport chunks cut inside a JSON string value, a key, and the fence opener.
|
|
187
|
+
// Exact concatenation must rebuild the message; a '\n' join would break it.
|
|
188
|
+
const valueCut = RAW.indexOf('fable') + 2;
|
|
189
|
+
const keyCut = RAW.indexOf('requestedProfile') + 5;
|
|
190
|
+
const splitValueParts = [RAW.slice(0, valueCut), RAW.slice(valueCut)];
|
|
191
|
+
const splitValuePath = writeParts('split-inside-value.jsonl', splitValueParts);
|
|
192
|
+
assert.strictEqual(finalAssistantText(parseTranscriptEvents(splitValuePath).events), RAW, 'split-inside-value parts must concatenate to the exact raw JSON');
|
|
193
|
+
assert.throws(() => JSON.parse(RAW.slice(0, valueCut) + '\n' + RAW.slice(valueCut)), 'a newline join inside a JSON string must not parse (the #308 reviewer case)');
|
|
194
|
+
const POSITIVE_SPLITS = [
|
|
195
|
+
['split inside a JSON string value', splitValueParts],
|
|
196
|
+
['split inside a JSON key', [RAW.slice(0, keyCut), RAW.slice(keyCut)]],
|
|
197
|
+
['split inside the fence opener', ['```js', 'on\n' + JSON.stringify(SELECTION, null, 2) + '\n```']],
|
|
198
|
+
['fenced JSON block followed by a harness-label part', [FENCED, LABEL]],
|
|
199
|
+
];
|
|
200
|
+
POSITIVE_SPLITS.forEach(([name, parts], index) => {
|
|
201
|
+
const result = grade(jsonOnlyExpectation, jsonOnlyDir, writeParts(`positive-split-${index}.jsonl`, parts));
|
|
202
|
+
assert.strictEqual(result.passed, true, `${name} should pass end to end: ${result.results[0].reason}`);
|
|
203
|
+
});
|
|
204
|
+
const NEGATIVE_SPLITS = [
|
|
205
|
+
['prose chunk glued to JSON start', ['Here:', RAW], /prose before/],
|
|
206
|
+
['prose after a split JSON', [RAW.slice(0, 10), RAW.slice(10), ' done'], /not a single valid JSON value/],
|
|
207
|
+
];
|
|
208
|
+
NEGATIVE_SPLITS.forEach(([name, parts, expected], index) => {
|
|
209
|
+
const result = grade(jsonOnlyExpectation, jsonOnlyDir, writeParts(`negative-split-${index}.jsonl`, parts));
|
|
210
|
+
assert.strictEqual(result.passed, false, `${name} must fail end to end`);
|
|
211
|
+
assert.match(result.results[0].reason, expected, `${name} should give a clear reason`);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Claude stream-json: assistant content parts plus a terminal `result` that
|
|
215
|
+
// repeats them. The repeat must not be double-counted, and must not mask prose.
|
|
216
|
+
const writeClaude = (name, contentTexts, resultText) => {
|
|
217
|
+
const transcriptPath = path.join(jsonOnlyDir, name);
|
|
218
|
+
const lines = [
|
|
219
|
+
{ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'tu-1', name: 'Read', input: { file_path: 'profiles.md' } }] } },
|
|
220
|
+
{ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 'tu-1', content: 'ok' }] } },
|
|
221
|
+
{ type: 'assistant', message: { content: contentTexts.map(text => ({ type: 'text', text })) } },
|
|
222
|
+
{ type: 'result', subtype: 'success', result: resultText },
|
|
223
|
+
];
|
|
224
|
+
fs.writeFileSync(transcriptPath, lines.map(line => JSON.stringify(line)).join('\n'));
|
|
225
|
+
return transcriptPath;
|
|
226
|
+
};
|
|
227
|
+
assert.strictEqual(grade(jsonOnlyExpectation, jsonOnlyDir, writeClaude('claude-result-repeat.jsonl', [FENCED], FENCED)).passed, true, 'Claude result repeat must not double the JSON');
|
|
228
|
+
assert.strictEqual(grade(jsonOnlyExpectation, jsonOnlyDir, writeClaude('claude-prose-parts.jsonl', ['Here is the record.', FENCED], FENCED)).passed, false, 'Claude prose content part must fail even when result holds only JSON');
|
|
229
|
+
} finally {
|
|
230
|
+
fs.rmSync(jsonOnlyDir, { recursive: true, force: true });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log(`Behavioral case validation passed (${fs.readdirSync(casesDir).filter(name => name.endsWith('.yaml')).length} cases + negative controls + ordered execution evidence + json-only final output).`);
|
|
@@ -183,6 +183,14 @@ evidence supports project-scope `.js` loading and edit/verification state on
|
|
|
183
183
|
OpenCode 1.18.31 (macOS): [live-host evidence](../benchmarks/results/live-host/opencode-2026-09-16/README.md). The
|
|
184
184
|
retained snapshot predates #190 and remains historical evidence only. Current OpenCode removes the permanent post-reflection hard lock; only the third-failure reflection boundary pauses edits. Reflection was operator-seeded, then agent-rewritten, so do not promote this into a behavioral-effectiveness claim. `.mjs` auto-discovery is broken on this host version (issue #127). npm-package installation and behavioral V2-hook fire remain unverified.
|
|
185
185
|
|
|
186
|
+
The plugin verification runner uses asynchronous child processes with a shared
|
|
187
|
+
sequence budget and bounded streamed diagnostics. Deterministic mechanism
|
|
188
|
+
coverage checks event-loop responsiveness, large-output verdicts, timeout/process
|
|
189
|
+
cleanup, and concurrent edit/session reset handling. The standalone manual CLI
|
|
190
|
+
retains its synchronous per-command runner. This change supplies no additional
|
|
191
|
+
live-host evidence; the historical installation and behavior boundaries above
|
|
192
|
+
remain unchanged.
|
|
193
|
+
|
|
186
194
|
## Official sources
|
|
187
195
|
|
|
188
196
|
The matrix records the official documentation reviewed for each platform. Important source families include [OpenAI plugin packaging](https://developers.openai.com/plugins/build/plugins), [OpenAI public submission](https://developers.openai.com/plugins/deploy/submission), [Codex skills](https://learn.chatgpt.com/docs/build-skills), [Codex hooks](https://learn.chatgpt.com/docs/hooks), [Claude skills](https://code.claude.com/docs/en/skills), [OpenCode skills](https://opencode.ai/docs/skills), [GitHub Copilot Agent Skills](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-cloud-agent/add-skills), [Cursor skills](https://cursor.com/docs/skills), [Continue rules](https://docs.continue.dev/customize/rules), and [Hermes skills](https://hermes-agent.nousresearch.com/docs/user-guide/features/skills).
|
|
@@ -247,11 +247,12 @@ function globToRegExp(pattern) {
|
|
|
247
247
|
return new RegExp(`^${body}$`);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
//
|
|
250
|
+
// Keep 64-bit device/file IDs exact (Windows IDs can exceed Number's safe
|
|
251
|
+
// integer range), while still rejecting symlink and hard-link aliases.
|
|
251
252
|
function sameFile(a, b) {
|
|
252
|
-
const x = fs.statSync(a);
|
|
253
|
-
const y = fs.statSync(b);
|
|
254
|
-
return x.ino >
|
|
253
|
+
const x = fs.statSync(a, { bigint: true });
|
|
254
|
+
const y = fs.statSync(b, { bigint: true });
|
|
255
|
+
return x.ino > 0n && x.ino === y.ino && x.dev === y.dev;
|
|
255
256
|
}
|
|
256
257
|
|
|
257
258
|
// Open without blocking (O_NONBLOCK keeps a FIFO from waiting for a writer), then require a regular file before reading.
|
|
@@ -101,7 +101,7 @@ function isUncPathPrefix(token, command, index) {
|
|
|
101
101
|
return token === '' && command[index] === '\\' && command[index + 1] === '\\';
|
|
102
102
|
}
|
|
103
103
|
// Canonical SHA-256 of harness-everything/scripts/doc-check.js (CRLF normalised to LF). Re-pin deliberately when that script changes.
|
|
104
|
-
const SHIPPED_DOC_CHECK_DIGEST = '
|
|
104
|
+
const SHIPPED_DOC_CHECK_DIGEST = '0e93f252b2350b145c34dcb5354aa3b4246f3c59e948a62c3a912305b355a475';
|
|
105
105
|
// This install's own doc-check runtime, relative to hooks/scripts/lib: the source checkout or the packaged plugin.
|
|
106
106
|
const DOC_CHECK_INSTALL_LAYOUTS = [
|
|
107
107
|
['..', '..', '..', 'harness-everything', 'scripts', 'doc-check.js'],
|
|
@@ -52,39 +52,37 @@ and on failure calls `client.session.prompt()` to push a synthetic follow-up
|
|
|
52
52
|
message into the session - forcing the agent to keep working instead of
|
|
53
53
|
actually stopping.
|
|
54
54
|
|
|
55
|
-
The
|
|
56
|
-
|
|
57
|
-
`HARNESS_VERIFY_TIMEOUT_MS
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
`execution_error` (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
follow-up in issue #338.
|
|
55
|
+
The async runner streams stdout/stderr and gives the **whole ordered sequence**
|
|
56
|
+
(`test`, `lint`, `build`) one wall-clock budget: 10 minutes by default, overridden
|
|
57
|
+
by `HARNESS_VERIFY_TIMEOUT_MS`. It reports progress every 15 seconds in the
|
|
58
|
+
plugin log. No later script starts after the budget expires. Each run is
|
|
59
|
+
classified as `pass` (exit 0), `test_failure` (a real nonzero exit), `timeout`
|
|
60
|
+
(the sequence deadline expired), or `execution_error` (spawn failure, external
|
|
61
|
+
signal, or cancellation). Only `test_failure` advances the Rule-of-3 counter.
|
|
62
|
+
|
|
63
|
+
| Resource | Bound / behavior |
|
|
64
|
+
|---|---|
|
|
65
|
+
| Retained output | First and last 2,000 characters per stream; the final diagnostic shares a 2,000-character budget across streams, plus labels/elision markers |
|
|
66
|
+
| Failure identity | Incremental whitespace-normalized digest of both complete streams; first recognized test marker and suite context tracked separately |
|
|
67
|
+
| Marker parsing | First 4,096 characters of each line; an overlong marker/context selects the complete-stream digest; ordinary long banners preserve later complete markers |
|
|
68
|
+
| Timeout/cancel | Terminate the shell/npm/script process group, then force-kill after 150 ms on POSIX; Windows uses `taskkill /T /F` |
|
|
69
|
+
| Concurrent lifecycle | Duplicate idle events share the active run; a code edit or session reset aborts it; an idle arriving during cancellation waits for cleanup before verifying the newer edits; the V2 event stream continues consuming cancellation events; disposal awaits active child cleanup; stale results cannot clear newer edits or recreate deleted state |
|
|
70
|
+
|
|
71
|
+
The event loop remains available while scripts run. The output-retention limit
|
|
72
|
+
never becomes a check verdict. Mechanism tests cover large output, shared
|
|
73
|
+
budget, child cleanup and concurrent hooks; these are deterministic mechanism
|
|
74
|
+
evidence, not a new live-host verification claim. Processes deliberately
|
|
75
|
+
escaping the process group are outside this cleanup boundary.
|
|
77
76
|
|
|
78
77
|
### 3. Rule-of-3 zoom-out boundary (`tool.execute.before` + the verification observer above)
|
|
79
78
|
|
|
80
79
|
Enforces Rule of 3 on repeated verification failures (same failing command +
|
|
81
80
|
normalized failure evidence: the first test-level failing marker - TAP
|
|
82
81
|
`not ok`, node:test/jest symbols, Jest test blocks, assertion lines - over the
|
|
83
|
-
|
|
82
|
+
streamed stdout **and** stderr, with a preceding suite header such as
|
|
84
83
|
Jest's `FAIL <file>` carried as context so different tests in one file stay
|
|
85
84
|
distinct; without a test-level marker the signature falls back to a digest of
|
|
86
|
-
|
|
87
|
-
only to the user-visible diagnostic excerpt, never to signature analysis; not
|
|
85
|
+
those complete streams, never to a suite header alone; the diagnostic retention bound does not limit the stream digest; not
|
|
88
86
|
arbitrary tool failures, since
|
|
89
87
|
`tool.execute.after` has no normalized success/failure field to key a
|
|
90
88
|
signature on for tools in general):
|
|
@@ -146,17 +144,17 @@ installation surfaces and were not verified in this run.
|
|
|
146
144
|
|
|
147
145
|
## Manual verification CLI
|
|
148
146
|
|
|
149
|
-
**File:** `hooks/verify.js` -
|
|
150
|
-
`index.mjs`'s verification gate, kept as a standalone command:
|
|
147
|
+
**File:** `hooks/verify.js` - a standalone "run available npm scripts" command:
|
|
151
148
|
|
|
152
149
|
```bash
|
|
153
150
|
node opencode-plugin/hooks/verify.js
|
|
154
151
|
```
|
|
155
152
|
|
|
156
153
|
Run it from the workspace under test, never from the Harness repo root - from
|
|
157
|
-
there it would re-enter `npm test` from inside `npm test`.
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
there it would re-enter `npm test` from inside `npm test`. The manual CLI retains its synchronous per-command budget and 16 MiB capture
|
|
155
|
+
limit; the async sequence budget and streaming behavior above apply to the
|
|
156
|
+
plugin only. Keeping its implementation separate preserves the single-file
|
|
157
|
+
plugin installation contract.
|
|
160
158
|
|
|
161
159
|
`plugin.json` records this standalone helper for repository parity checks; it
|
|
162
160
|
does not configure opencode. The runtime module is `index.mjs`.
|