harness-everything 0.47.2 → 0.47.3

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.
@@ -79,7 +79,7 @@ for (let i = 0; i < DEFAULTS.length; i += 1) {
79
79
  assert.ok(/timed out after/.test(source), `${label} labels timeout failures as timeouts`);
80
80
  assert.ok(/test_failure/.test(source), `${label} classifies confirmed failing checks`);
81
81
  assert.ok(/execution_error/.test(source), `${label} classifies spawn-level execution errors`);
82
- assert.ok(/maxBuffer/.test(source), `${label} bounds captured output instead of using the 1 MiB default`);
82
+ assert.ok(i === 0 ? /VERIFY_LINE_LIMIT = 4096/.test(source) : /maxBuffer/.test(source), `${label} bounds retained output`);
83
83
  const declared = Number(source.match(/^const DEFAULT_VERIFY_TIMEOUT_MS = (\d+);?$/m)[1]);
84
84
  assert.ok(declared >= 300000, `${label} default budget (${declared}ms) clears a several-minute suite`);
85
85
  }
@@ -95,15 +95,12 @@ assert.strictEqual(
95
95
  extractConst(cliSource, 'VERIFY_TIMEOUT_ENV').match(/'([^']*)'/)[1],
96
96
  'the plugin and the manual CLI read the same env override',
97
97
  );
98
- assert.strictEqual(
99
- extractConst(pluginSource, 'VERIFY_MAX_BUFFER').match(/= (\d+ \* \d+ \* \d+)/)[1],
100
- extractConst(cliSource, 'VERIFY_MAX_BUFFER').match(/= (\d+ \* \d+ \* \d+)/)[1],
101
- 'the plugin and the manual CLI allow the same output buffer',
102
- );
98
+ // The plugin now streams output; the standalone CLI retains its 16 MiB
99
+ // synchronous capture. Actual async runner contracts are exercised in 338.
103
100
 
104
101
  // Behavior: budget resolution.
105
102
  {
106
- const context = loadVerificationLogic(pluginSource);
103
+ const context = loadVerificationLogic(cliSource);
107
104
  assert.strictEqual(context.verifyTimeoutMs(), 600000, 'unset env uses the default budget');
108
105
  for (const value of ['abc', '0', '-5', '']) {
109
106
  context.process.env.HARNESS_VERIFY_TIMEOUT_MS = value;
@@ -114,167 +111,9 @@ assert.strictEqual(
114
111
  assert.ok(!Number.isNaN(Number(context.verifyTimeoutMs())), 'the resolved budget is numeric');
115
112
  }
116
113
 
117
- // Behavior: outcome classification per failure kind.
118
- {
119
- const context = loadVerificationLogic(pluginSource);
120
- const calls = [];
121
- context.execSync = (command, options) => {
122
- calls.push({ command, options });
123
- return '';
124
- };
125
- let result = context.runVerification('/nonexistent-workspace');
126
- assert.strictEqual(result.allPassed, true, 'a passing script verifies');
127
- assert.strictEqual(result.results[0].outcome, 'pass', 'a passing script is classified pass');
128
- assert.strictEqual(calls.length, 1, 'one command ran');
129
- assert.strictEqual(calls[0].command, 'npm test', 'the test script is the command');
130
- assert.strictEqual(calls[0].options.timeout, 600000, 'execSync receives the default budget');
131
- assert.strictEqual(calls[0].options.maxBuffer, 16 * 1024 * 1024, 'execSync receives the raised output buffer');
132
-
133
- context.process.env.HARNESS_VERIFY_TIMEOUT_MS = '120';
134
- calls.length = 0;
135
- context.execSync = (command, options) => {
136
- calls.push({ command, options });
137
- return '';
138
- };
139
- context.runVerification('/nonexistent-workspace');
140
- assert.strictEqual(calls[0].options.timeout, 120, 'execSync receives the overridden budget');
141
-
142
- delete context.process.env.HARNESS_VERIFY_TIMEOUT_MS;
143
- context.execSync = () => {
144
- const error = new Error('spawnSync npm test ETIMEDOUT');
145
- error.code = 'ETIMEDOUT';
146
- error.signal = 'SIGTERM'; // the live execSync timeout kill shape
147
- error.stdout = 'Harness OS - Self-Regression Test Suite\n';
148
- throw error;
149
- };
150
- result = context.runVerification('/nonexistent-workspace');
151
- assert.strictEqual(result.allPassed, false, 'a timeout is a verification failure');
152
- assert.strictEqual(result.results[0].timedOut, true, 'the timeout is recorded as a timeout');
153
- assert.strictEqual(result.results[0].outcome, 'timeout', 'a timeout is classified as timeout, not test_failure');
154
- assert.ok(/timed out after 600s/.test(result.results[0].error), 'the failure names the budget');
155
- assert.ok(/HARNESS_VERIFY_TIMEOUT_MS/.test(result.results[0].error), 'the failure names the override');
156
- assert.ok(/Self-Regression/.test(result.results[0].error), 'the failure keeps the last output for diagnosis');
157
-
158
- context.process.env.HARNESS_VERIFY_TIMEOUT_MS = '50';
159
- context.execSync = () => {
160
- const started = Date.now();
161
- while (Date.now() - started < 120) { /* simulate a kill at the budget without an error code */ }
162
- const error = new Error('Command failed: npm test');
163
- error.stdout = 'partial output';
164
- throw error;
165
- };
166
- result = context.runVerification('/nonexistent-workspace');
167
- assert.strictEqual(result.results[0].timedOut, true, 'a kill at the budget is recognized without an ETIMEDOUT code');
168
- assert.strictEqual(result.results[0].outcome, 'timeout', 'a kill at the budget is classified as timeout');
169
-
170
- // P2: a signal is external termination; only budget expiry is a timeout.
171
- context.process.env.HARNESS_VERIFY_TIMEOUT_MS = '600000';
172
- context.execSync = () => {
173
- const error = new Error('Command failed: npm test');
174
- error.signal = 'SIGTERM'; // killed early by a user, host, or process manager
175
- throw error; // status stays null: no verdict from the suite
176
- };
177
- result = context.runVerification('/nonexistent-workspace');
178
- assert.strictEqual(result.results[0].outcome, 'execution_error', 'an early external SIGTERM is an execution error, not a timeout');
179
- assert.strictEqual(result.results[0].timedOut, false, 'an early external SIGTERM does not claim a timeout');
180
- assert.strictEqual(result.results[0].signal, 'SIGTERM', 'the signal is retained for diagnostics');
181
- assert.ok(/signal SIGTERM/.test(result.results[0].error), 'the execution error names the signal');
182
-
183
- context.process.env.HARNESS_VERIFY_TIMEOUT_MS = '50';
184
- context.execSync = () => {
185
- const started = Date.now();
186
- while (Date.now() - started < 80) { /* killed once the budget already elapsed, like execSync's own timeout */ }
187
- const error = new Error('spawnSync npm test SIGTERM');
188
- error.signal = 'SIGTERM';
189
- throw error;
190
- };
191
- result = context.runVerification('/nonexistent-workspace');
192
- assert.strictEqual(result.results[0].outcome, 'timeout', 'a signal kill with no exit status once the budget elapsed is a timeout');
193
-
194
- context.execSync = () => {
195
- const started = Date.now();
196
- while (Date.now() - started < 80) { /* slow, but it still produced a verdict */ }
197
- const error = new Error('Command failed: npm test');
198
- error.status = 1;
199
- error.stdout = 'confirmed failure after a slow run';
200
- throw error;
201
- };
202
- result = context.runVerification('/nonexistent-workspace');
203
- assert.strictEqual(result.results[0].outcome, 'test_failure', 'a real exit status stays a confirmed failure even past the budget');
204
- assert.strictEqual(result.results[0].timedOut, false, 'a confirmed verdict is not a timeout');
205
-
206
- context.process.env.HARNESS_VERIFY_TIMEOUT_MS = '600000';
207
- context.execSync = () => {
208
- const error = new Error('Command failed: npm test');
209
- error.status = 1;
210
- error.stdout = 'real assertion failure';
211
- throw error;
212
- };
213
- result = context.runVerification('/nonexistent-workspace');
214
- assert.strictEqual(result.results[0].timedOut, false, 'a fast failure is not mislabeled as a timeout');
215
- assert.strictEqual(result.results[0].outcome, 'test_failure', 'a non-zero exit is a confirmed test failure');
216
- assert.strictEqual(result.results[0].error, 'real assertion failure', 'a fast failure reports its output');
217
-
218
- context.execSync = () => {
219
- const error = new Error('spawnSync npm test ENOBUFS');
220
- error.code = 'ENOBUFS';
221
- error.stdout = 'x'.repeat(300);
222
- throw error;
223
- };
224
- result = context.runVerification('/nonexistent-workspace');
225
- assert.strictEqual(result.results[0].outcome, 'execution_error', 'ENOBUFS is an execution error, not a test failure');
226
- assert.ok(/execution error: ENOBUFS/.test(result.results[0].error), 'the execution error names its cause');
227
- assert.ok(!/timed out/.test(result.results[0].error), 'ENOBUFS is not reported as a timeout');
228
-
229
- context.execSync = () => {
230
- const error = new Error('spawnSync npm test ENOENT');
231
- error.code = 'ENOENT';
232
- throw error;
233
- };
234
- result = context.runVerification('/nonexistent-workspace');
235
- assert.strictEqual(result.results[0].outcome, 'execution_error', 'a missing executable is an execution error');
236
- assert.strictEqual(result.results[0].exitCode, null, 'a spawn-level error has no exit status');
237
- }
238
-
239
- // Behavior: test_failure evidence spans both streams in full (P1).
240
- {
241
- const context = loadVerificationLogic(pluginSource);
242
- context.process.env.HARNESS_VERIFY_TIMEOUT_MS = '600000';
243
- context.execSync = () => {
244
- const error = new Error('Command failed: npm test');
245
- error.status = 1;
246
- error.stdout = '> fixture@0.0.0 test\n> node fixture.js\n';
247
- error.stderr = 'AssertionError [ERR_ASSERTION]: alpha boom\n';
248
- throw error;
249
- };
250
- const bothStreams = context.runVerification('/nonexistent-workspace').results[0];
251
- assert.strictEqual(bothStreams.outcome, 'test_failure', 'a both-stream failure is a confirmed test failure');
252
- assert.ok(/alpha boom/.test(bothStreams.error), 'the user-visible diagnostic includes the stderr assertion');
253
- assert.ok(/node fixture\.js/.test(bothStreams.error), 'the user-visible diagnostic keeps the stdout banner');
254
- assert.ok(/alpha boom/.test(bothStreams.evidence), 'the kept evidence includes stderr');
255
- assert.ok(/node fixture\.js/.test(bothStreams.evidence), 'the kept evidence includes stdout');
256
-
257
- context.execSync = () => {
258
- const error = new Error('Command failed: npm test');
259
- error.status = 1;
260
- error.stdout = `${'A'.repeat(2000)}TAIL-MARKER`;
261
- throw error;
262
- };
263
- const longOutput = context.runVerification('/nonexistent-workspace').results[0];
264
- assert.ok(longOutput.error.length < 2200, 'the user-visible diagnostic stays bounded');
265
- assert.ok(/TAIL-MARKER/.test(longOutput.error), 'the trailing failure evidence survives diagnostic truncation');
266
- assert.ok(/chars omitted/.test(longOutput.error), 'the elision is explicit in the diagnostic');
267
- assert.ok(longOutput.evidence.length > 2000 && /TAIL-MARKER/.test(longOutput.evidence), 'the full evidence is retained untruncated for signature analysis');
268
-
269
- context.execSync = () => {
270
- const error = new Error('Command failed: npm test');
271
- error.code = 'ENOBUFS';
272
- error.stdout = `${'A'.repeat(2000)}TAIL-MARKER`;
273
- throw error;
274
- };
275
- const spawnError = context.runVerification('/nonexistent-workspace').results[0];
276
- assert.strictEqual(spawnError.evidence, undefined, 'only confirmed test failures keep full evidence');
277
- }
114
+ // Plugin outcome, evidence and cleanup behavior is covered with real child
115
+ // processes in mechanism-338-opencode-async.test.js, and downstream Rule-of-3
116
+ // behavior remains in mechanism-39-opencode-verify-outcomes.test.js.
278
117
 
279
118
  // Behavior: the manual CLI mirror classifies the same way.
280
119
  {
@@ -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).
@@ -4,7 +4,7 @@ description: "Route software/project work through Harness: classify tier/topolog
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Miya Daniel
7
- version: 0.47.1
7
+ version: 0.47.3
8
8
  ---
9
9
 
10
10
  # Harness Everything
@@ -247,11 +247,12 @@ function globToRegExp(pattern) {
247
247
  return new RegExp(`^${body}$`);
248
248
  }
249
249
 
250
- // Same file when both paths stat to one device and inode, so symlinks and hard links are caught.
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 > 0 && x.ino === y.ino && x.dev === y.dev;
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 = '3ca386666d4dae9a98ffa27adf2f326bf171a783af90515375f0b328ca35f194';
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 verification run has a budget, not a fixed 60s cap: a healthy self-regression
56
- suite takes around two minutes, so the default is 10 minutes and
57
- `HARNESS_VERIFY_TIMEOUT_MS` overrides it. Every run is classified as `pass`
58
- (exit 0), `test_failure` (a real non-zero exit), `timeout` (the budget actually
59
- expired: `ETIMEDOUT`, or a kill with no exit status once the budget elapsed) or
60
- `execution_error` (a spawn-level failure such as `ENOBUFS`/`ENOENT`, or an
61
- external signal such as `SIGTERM`/`SIGINT` before the budget - a signal alone
62
- is not a budget expiry).
63
- Only `test_failure` confirms a failing check and advances the Rule-of-3
64
- counter; `timeout` and `execution_error` are reported as uncompleted
65
- verifications and never impersonate repeated failures. A timeout is reported as
66
- `timed out after <n>s (set HARNESS_VERIFY_TIMEOUT_MS to raise it); last
67
- output: ...`, a spawn-level failure as `could not run to completion (execution
68
- error: ENOBUFS); output: ...` (an external signal names the signal).
69
-
70
- Known operational limits of the synchronous runner: `execSync` blocks the
71
- plugin's event loop for the duration of each script (worst case one budget per
72
- script), and captured output is buffered up to 16 MiB. A run whose output
73
- exceeds that buffer is classified as an `execution_error`, never as a failed
74
- check, so buffer pressure cannot flip the circuit breaker. Moving verification
75
- to an async `spawn` with streaming, bounded diagnostics is tracked as a
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
- full captured stdout **and** stderr, with a preceding suite header such as
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
- that full evidence, never to a suite header alone; the 2 KiB bound applies
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` - the same "run available npm scripts" logic as
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`. It is intentionally
158
- a separate copy of the logic, not a shared import, so `index.mjs` stays a
159
- single portable file for the installation step above.
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`.