harness-everything 0.35.0 → 0.36.1

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.
@@ -52,11 +52,42 @@ 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.
77
+
55
78
  ### 3. Rule-of-3 zoom-out boundary (`tool.execute.before` + the verification observer above)
56
79
 
57
80
  Enforces Rule of 3 on repeated verification failures (same failing command +
58
- truncated error, not arbitrary tool failures - `tool.execute.after` has no
59
- normalized success/failure field to key a signature on for tools in general):
81
+ normalized failure evidence: the first test-level failing marker - TAP
82
+ `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
84
+ Jest's `FAIL <file>` carried as context so different tests in one file stay
85
+ 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
88
+ arbitrary tool failures, since
89
+ `tool.execute.after` has no normalized success/failure field to key a
90
+ signature on for tools in general):
60
91
  - 3rd failure on the same signature forces a reflection message instead of a
61
92
  retry nudge
62
93
  - the follow-up asks for a tokenized `zoom-out-report.md` artifact with the
@@ -105,19 +105,91 @@ function getAvailableScripts() {
105
105
  }
106
106
  }
107
107
 
108
+ // Mirrors ../index.mjs's verification budget and outcome classification so
109
+ // the manual CLI cannot drift from the plugin. A 60s cap killed healthy
110
+ // ~2 minute self-regression runs. Only `test_failure` (a real non-zero exit)
111
+ // is a confirmed failing check; `timeout` and `execution_error` mean the run
112
+ // produced no verdict. A signal alone is not a budget timeout - only expiry
113
+ // (ETIMEDOUT, or a kill with no exit status once the budget elapsed) is.
114
+ const VERIFY_TIMEOUT_ENV = 'HARNESS_VERIFY_TIMEOUT_MS';
115
+ const DEFAULT_VERIFY_TIMEOUT_MS = 600000;
116
+ const VERIFY_MAX_BUFFER = 16 * 1024 * 1024;
117
+ const VERIFY_OUTPUT_LIMIT = 2000;
118
+
119
+ function verifyTimeoutMs() {
120
+ const raw = Number(process.env[VERIFY_TIMEOUT_ENV]);
121
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_VERIFY_TIMEOUT_MS;
122
+ }
123
+
124
+ // Full failure evidence for signature analysis (stdout and stderr both, in
125
+ // full) - kept identical to ../index.mjs's inlined copy.
126
+ function verificationEvidence(stdout, stderr) {
127
+ const out = String(stdout || '');
128
+ const err = String(stderr || '');
129
+ if (out && err) return `${out}\nstderr:\n${err}`;
130
+ return out || err;
131
+ }
132
+
133
+ // Bounded excerpt that keeps both ends (npm banner plus failure tail).
134
+ function boundedDiagnostic(text, limit) {
135
+ const value = String(text || '');
136
+ if (value.length <= limit) return value;
137
+ const half = Math.floor(limit / 2);
138
+ return `${value.slice(0, half)}\n... [${value.length - half * 2} chars omitted] ...\n${value.slice(-half)}`;
139
+ }
140
+
141
+ // User-visible diagnostic: both streams get their own budget; a single
142
+ // non-empty stream stays label-free.
143
+ function verificationDiagnostic(stdout, stderr) {
144
+ const out = String(stdout || '');
145
+ const err = String(stderr || '');
146
+ if (out && err) {
147
+ const share = Math.floor(VERIFY_OUTPUT_LIMIT / 2);
148
+ return `stdout:\n${boundedDiagnostic(out, share)}\nstderr:\n${boundedDiagnostic(err, share)}`;
149
+ }
150
+ return boundedDiagnostic(out || err, VERIFY_OUTPUT_LIMIT);
151
+ }
152
+
153
+ function classifyVerificationError(error, startedAt, timeout) {
154
+ const code = String((error && error.code) || '');
155
+ const signal = String((error && error.signal) || '');
156
+ const exitCode = error && Number.isInteger(error.status) ? error.status : null;
157
+ const budgetExpired =
158
+ code === 'ETIMEDOUT' || (exitCode === null && Date.now() - startedAt >= timeout);
159
+ if (budgetExpired) return { outcome: 'timeout', exitCode, signal, timedOut: true };
160
+ if (exitCode === null || ['ENOBUFS', 'ENOENT', 'EACCES', 'EAGAIN', 'ENOMEM'].includes(code)) {
161
+ return { outcome: 'execution_error', exitCode, signal, timedOut: false };
162
+ }
163
+ return { outcome: 'test_failure', exitCode, signal, timedOut: false };
164
+ }
165
+
108
166
  function runVerification(command) {
167
+ const timeout = verifyTimeoutMs();
168
+ const startedAt = Date.now();
109
169
  try {
110
- execSync(command, {
170
+ execSync(command, {
111
171
  stdio: 'pipe',
112
- timeout: 60000 // 60 second timeout
172
+ timeout,
173
+ maxBuffer: VERIFY_MAX_BUFFER
113
174
  });
114
- return { success: true, command };
175
+ return { success: true, command, outcome: 'pass', exitCode: 0, signal: null, timedOut: false };
115
176
  } catch (e) {
116
- return {
117
- success: false,
118
- command,
119
- error: e.message.slice(0, 200)
120
- };
177
+ const stdout = (e.stdout || '').toString();
178
+ const stderr = (e.stderr || '').toString();
179
+ const output = verificationEvidence(stdout, stderr) || String((e && e.message) || '');
180
+ const { outcome, exitCode, signal, timedOut } = classifyVerificationError(e, startedAt, timeout);
181
+ let error;
182
+ if (outcome === 'timeout') {
183
+ error = `timed out after ${Math.round(timeout / 1000)}s (set ${VERIFY_TIMEOUT_ENV} to raise it)`;
184
+ } else if (outcome === 'execution_error') {
185
+ const reason = String((e && e.code) || (signal ? `signal ${signal}` : 'no exit status'));
186
+ error = `could not run to completion (execution error: ${reason}); output: ${output.slice(0, 200).replace(/\s+/g, ' ').trim() || '(none)'}`;
187
+ } else {
188
+ error = verificationDiagnostic(stdout, stderr) || String((e && e.message) || '');
189
+ }
190
+ const result = { success: false, command, outcome, exitCode, signal, timedOut, error };
191
+ if (outcome === 'test_failure' && output) result.evidence = output;
192
+ return result;
121
193
  }
122
194
  }
123
195
 
@@ -164,11 +236,13 @@ function main() {
164
236
  }
165
237
  }
166
238
 
167
- // Update state
239
+ // Update state. The full `evidence` strings are analysis-only (they can
240
+ // approach VERIFY_MAX_BUFFER); persist just the bounded diagnostic so
241
+ // edit-state.json stays small.
168
242
  state.verificationPending = !allPassed;
169
243
  state.editsSinceVerification = allPassed ? 0 : state.editsSinceVerification;
170
244
  state.lastVerificationTime = Date.now();
171
- state.verificationResults = results;
245
+ state.verificationResults = results.map(({ evidence, ...rest }) => rest);
172
246
 
173
247
  saveState(state);
174
248
 
@@ -179,7 +253,9 @@ function main() {
179
253
  allPassed,
180
254
  results: results.map(r => ({
181
255
  command: r.command,
182
- success: r.success
256
+ success: r.success,
257
+ outcome: r.outcome,
258
+ timedOut: r.timedOut === true
183
259
  })),
184
260
  message: allPassed
185
261
  ? 'All available verification passed. You can now complete.'
@@ -21,12 +21,18 @@
21
21
  * once locked) + failure tracking inside session.idle
22
22
  * compliance.js -> folded into the same state writes, no separate stage
23
23
  *
24
- * The circuit breaker trips on repeated *verification* failures (same failing
25
- * command + truncated error), not on arbitrary tool failures: tool.execute.after
24
+ * The circuit breaker trips on repeated *verification* failures of the same
25
+ * underlying check (same command + normalized failure evidence: the first
26
+ * test-level failing marker with its suite header as context, or a digest of
27
+ * the full captured stdout+stderr when no test-level marker exists), not on
28
+ * arbitrary tool failures: tool.execute.after
26
29
  * gives back `{title, output, metadata}` for every tool with no normalized
27
30
  * success/failure field, so there is no reliable cross-tool failure signal to
28
31
  * key a signature on. Verification failures are self-produced and already
29
- * normalized, so that is the honest signal to use.
32
+ * normalized, so that is the honest signal to use. Uncompleted verifications
33
+ * (timeout, execution error such as ENOBUFS/ENOENT) are reported as failures
34
+ * but never counted toward the same-check threshold, because no failing check
35
+ * was actually confirmed.
30
36
  */
31
37
 
32
38
  import { homedir } from "node:os"
@@ -269,8 +275,84 @@ function defaultCompliance() {
269
275
  }
270
276
  }
271
277
 
272
- function extractFailureSignature(text) {
273
- return text.slice(0, 100).replace(/\s+/g, " ").trim()
278
+ // Test-level evidence of an actual failing check, in line order: TAP, Node
279
+ // test-runner symbols, Jest test blocks, indented Mocha numbering, and
280
+ // assertion/error lines (which may also be indented). `✖ failing tests:`
281
+ // (the node:test summary header) is skipped in favor of the specific test
282
+ // line that follows it, and Jest's `● Console` section is not failure
283
+ // evidence: neither the heading nor its indented payload (logged output,
284
+ // including deeper-indented lines beginning with "●") becomes a marker.
285
+ const FAILURE_MARKER_PATTERNS = [/^not ok\b.*$/m, /^\s*[✖✕]\s*(?!\s*failing\b).+$/m, /^\s*●\s+(?!Console\b|Test suite failed to run\b|Validation Error\b).+$/m, /^[ \t]*\d+\)[ \t]+.+$/m, /^[ \t]*[A-Za-z]*Error\b.*$/m];
286
+
287
+ // Suite/file-level context, never an identity on its own: Jest prints
288
+ // `FAIL <file>` (or `● Test suite failed to run`) above the failing test
289
+ // blocks, so different tests in one file would otherwise share a signature.
290
+ // A context line is carried into the signature only when a test-level marker
291
+ // follows it; without one, the full-evidence digest decides.
292
+ const CONTEXT_MARKER_PATTERNS = [/^FAIL\b.*$/m, /^\s*●\s+(?:Test suite failed to run|Validation Error)\b.*$/m];
293
+
294
+ function normalizeFailureLine(line) {
295
+ let text = String(line || "").replace(/\s+/g, " ").trim();
296
+ text = text.replace(/\s*\(\d+(?:\.\d+)?\s*m?s\)\s*$/, ""); // runner timing suffixes
297
+ text = text.replace(/^not ok \d+\b/, "not ok"); // TAP plan numbering
298
+ return text.slice(0, 160);
299
+ }
300
+
301
+ function firstFailureMarker(text) {
302
+ const lines = String(text || "").replace(/\u001b\[[0-9;]*m/g, "").split(/\r?\n/);
303
+ let context = null;
304
+ let consoleIndent = null;
305
+ for (const line of lines) {
306
+ const bullet = /^([ \t]*)●[ \t]+/.exec(line);
307
+ if (consoleIndent !== null) {
308
+ // Jest's Console block contains indented log payloads, which may
309
+ // themselves begin with "●". Only a peer/parent section bullet or a
310
+ // non-indented line may end the block; deeper bullets remain data.
311
+ if (bullet) {
312
+ if (bullet[1].length > consoleIndent) continue;
313
+ consoleIndent = null;
314
+ } else if (line.trim() === "" || /^[ \t]/.test(line)) {
315
+ continue;
316
+ } else {
317
+ consoleIndent = null;
318
+ }
319
+ }
320
+ if (bullet && /^Console\b/.test(line.slice(bullet[0].length))) {
321
+ consoleIndent = bullet[1].length;
322
+ continue;
323
+ }
324
+ if (FAILURE_MARKER_PATTERNS.some((pattern) => pattern.test(line))) {
325
+ const normalized = normalizeFailureLine(line);
326
+ if (normalized) return context ? `${context}: ${normalized}` : normalized;
327
+ }
328
+ if (CONTEXT_MARKER_PATTERNS.some((pattern) => pattern.test(line))) {
329
+ const normalized = normalizeFailureLine(line);
330
+ if (normalized) context = normalized;
331
+ }
332
+ }
333
+ return null;
334
+ }
335
+
336
+ // A stable, normalized signature: command + outcome + exit condition + the
337
+ // first test-level failing marker (with a suite/file context line when the
338
+ // runner printed one). Without a test-level marker the signature falls back
339
+ // to a digest of the full captured evidence (stdout and stderr both), so
340
+ // suite-header-only or banner-only prefixes from *different* failures can
341
+ // never collide (identical byte-for-byte evidence still matches, which is
342
+ // the honest reading: the same observation repeated). The user-visible
343
+ // `error` is a bounded diagnostic and is only the fallback when no full
344
+ // evidence was kept.
345
+ function buildFailureSignature(result) {
346
+ const evidence = String((result && (result.evidence || result.error)) || "");
347
+ const marker = firstFailureMarker(evidence);
348
+ const exit =
349
+ result && Number.isInteger(result.exitCode) ? `exit=${result.exitCode}`
350
+ : result && result.signal ? `signal=${result.signal}`
351
+ : "exit=unknown";
352
+ const base = `${(result && result.command) || "verification"} [${(result && result.outcome) || "unknown"}] ${exit}`;
353
+ if (marker) return `${base}: ${marker}`;
354
+ const digest = createHash("sha256").update(evidence.replace(/\s+/g, " ").trim()).digest("hex").slice(0, 16);
355
+ return `${base}: unrecognized-output ${digest}`;
274
356
  }
275
357
 
276
358
  function getAvailableScripts(cwd) {
@@ -283,6 +365,81 @@ function getAvailableScripts(cwd) {
283
365
  }
284
366
  }
285
367
 
368
+ // Verification budget for the npm scripts run on session.idle. The Harness
369
+ // self-regression suite needs ~2 minutes on a laptop, so the original 60s cap
370
+ // killed healthy runs and reported them as failures (same truncated stdout as
371
+ // the failure signature, three times in a row -> forced reflection).
372
+ // Known limits of this synchronous runner, kept honest: execSync blocks the
373
+ // plugin's event loop for the duration of each command (worst case one budget
374
+ // per script), and a command whose output exceeds VERIFY_MAX_BUFFER is
375
+ // classified as an execution error - never as a failed check - so buffer
376
+ // pressure cannot impersonate a test failure.
377
+ const VERIFY_TIMEOUT_ENV = "HARNESS_VERIFY_TIMEOUT_MS"
378
+ const DEFAULT_VERIFY_TIMEOUT_MS = 600000
379
+ const VERIFY_MAX_BUFFER = 16 * 1024 * 1024
380
+ const VERIFY_OUTPUT_LIMIT = 2000
381
+
382
+ function verifyTimeoutMs() {
383
+ const raw = Number(process.env[VERIFY_TIMEOUT_ENV])
384
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_VERIFY_TIMEOUT_MS
385
+ }
386
+
387
+ // Full failure evidence for signature analysis: both captured streams, in
388
+ // full. execSync already bounds each stream at VERIFY_MAX_BUFFER, and the old
389
+ // stdout-only 2 KiB prefix cut happened *before* buildFailureSignature() ran,
390
+ // so distinct failures behind a shared banner collapsed into one signature.
391
+ function verificationEvidence(stdout, stderr) {
392
+ const out = String(stdout || "")
393
+ const err = String(stderr || "")
394
+ if (out && err) return `${out}\nstderr:\n${err}`
395
+ return out || err
396
+ }
397
+
398
+ // Bounded, human-readable excerpt that keeps both ends: the npm banner at the
399
+ // head and the failure marker/summary at the tail survive truncation.
400
+ function boundedDiagnostic(text, limit) {
401
+ const value = String(text || "")
402
+ if (value.length <= limit) return value
403
+ const half = Math.floor(limit / 2)
404
+ return `${value.slice(0, half)}\n... [${value.length - half * 2} chars omitted] ...\n${value.slice(-half)}`
405
+ }
406
+
407
+ // User-visible diagnostic: both streams get their own budget, so a huge
408
+ // stdout cannot push a short stderr (where jest writes assertions) out of
409
+ // the report. A single non-empty stream stays label-free.
410
+ function verificationDiagnostic(stdout, stderr) {
411
+ const out = String(stdout || "")
412
+ const err = String(stderr || "")
413
+ if (out && err) {
414
+ const share = Math.floor(VERIFY_OUTPUT_LIMIT / 2)
415
+ return `stdout:\n${boundedDiagnostic(out, share)}\nstderr:\n${boundedDiagnostic(err, share)}`
416
+ }
417
+ return boundedDiagnostic(out || err, VERIFY_OUTPUT_LIMIT)
418
+ }
419
+
420
+ // Outcome taxonomy:
421
+ // pass the command exited 0
422
+ // test_failure a real non-zero exit status: a check actually failed
423
+ // timeout the configured budget expired: ETIMEDOUT, or a kill with
424
+ // no exit status once the budget elapsed: no verdict
425
+ // execution_error spawn-level failure, or an external signal (SIGTERM/
426
+ // SIGINT/SIGKILL from the user or host) before the budget:
427
+ // no verdict
428
+ // A signal alone is not a budget timeout - only expiry is. Only
429
+ // test_failure feeds the same-check Rule-of-3 counter.
430
+ function classifyVerificationError(error, startedAt, timeout) {
431
+ const code = String((error && error.code) || "")
432
+ const signal = String((error && error.signal) || "")
433
+ const exitCode = error && Number.isInteger(error.status) ? error.status : null
434
+ const budgetExpired =
435
+ code === "ETIMEDOUT" || (exitCode === null && Date.now() - startedAt >= timeout)
436
+ if (budgetExpired) return { outcome: "timeout", exitCode, signal, timedOut: true }
437
+ if (exitCode === null || ["ENOBUFS", "ENOENT", "EACCES", "EAGAIN", "ENOMEM"].includes(code)) {
438
+ return { outcome: "execution_error", exitCode, signal, timedOut: false }
439
+ }
440
+ return { outcome: "test_failure", exitCode, signal, timedOut: false }
441
+ }
442
+
286
443
  function runVerification(cwd) {
287
444
  const scripts = getAvailableScripts(cwd)
288
445
  const commands = [
@@ -295,13 +452,30 @@ function runVerification(cwd) {
295
452
  return { allPassed: true, skipped: true, results: [] }
296
453
  }
297
454
 
455
+ const timeout = verifyTimeoutMs()
298
456
  const results = []
299
457
  for (const { cmd } of commands) {
458
+ const startedAt = Date.now()
300
459
  try {
301
- execSync(cmd, { cwd, stdio: "pipe", timeout: 60000 })
302
- results.push({ command: cmd, success: true })
460
+ execSync(cmd, { cwd, stdio: "pipe", timeout, maxBuffer: VERIFY_MAX_BUFFER })
461
+ results.push({ command: cmd, success: true, outcome: "pass", exitCode: 0, signal: null, timedOut: false })
303
462
  } catch (e) {
304
- results.push({ command: cmd, success: false, error: (e.stdout || e.message || "").toString().slice(0, 200) })
463
+ const stdout = (e.stdout || "").toString()
464
+ const stderr = (e.stderr || "").toString()
465
+ const output = verificationEvidence(stdout, stderr) || String((e && e.message) || "")
466
+ const { outcome, exitCode, signal, timedOut } = classifyVerificationError(e, startedAt, timeout)
467
+ let error
468
+ if (outcome === "timeout") {
469
+ error = `timed out after ${Math.round(timeout / 1000)}s (set ${VERIFY_TIMEOUT_ENV} to raise it); last output: ${output.slice(0, 160).replace(/\s+/g, " ").trim() || "(none)"}`
470
+ } else if (outcome === "execution_error") {
471
+ const reason = String((e && e.code) || (signal ? `signal ${signal}` : "no exit status"))
472
+ error = `could not run to completion (execution error: ${reason}); output: ${output.slice(0, 200).replace(/\s+/g, " ").trim() || "(none)"}`
473
+ } else {
474
+ error = verificationDiagnostic(stdout, stderr) || String((e && e.message) || "")
475
+ }
476
+ const result = { command: cmd, success: false, outcome, exitCode, signal, timedOut, error }
477
+ if (outcome === "test_failure" && output) result.evidence = output
478
+ results.push(result)
305
479
  }
306
480
  }
307
481
  return { allPassed: results.every((r) => r.success), skipped: false, results }
@@ -697,29 +871,46 @@ export const HarnessEnforcement = async ({ client, directory }) => {
697
871
  return
698
872
  }
699
873
 
700
- const failing = results.find((r) => !r.success)
701
- const signature = extractFailureSignature(`${failing.command}: ${failing.error || ""}`)
702
- const trip = tripBreaker(breakerFile, signature)
874
+ const failing = results.filter((r) => !r.success)
875
+ const confirmed = failing.find((r) => r.outcome === "test_failure")
876
+ const primary = confirmed || failing[0]
703
877
  state.followUpPending = true
704
878
  saveJSON(editStateFile, state)
705
879
 
706
880
  let text
707
- if (trip.action === "force_reflection") {
708
- const { stateDir } = pathsFor(activeSession)
881
+ if (!confirmed) {
882
+ // Timeout or execution error: no check produced a verdict, so this
883
+ // must not advance the same-check Rule-of-3 counter. Report it as an
884
+ // uncompleted verification and stay honest about what is unknown.
885
+ const detail =
886
+ primary.outcome === "timeout"
887
+ ? `timed out (budget ${Math.round(verifyTimeoutMs() / 1000)}s; set ${VERIFY_TIMEOUT_ENV} to raise it)`
888
+ : "could not run to completion (execution error, not a confirmed test failure)"
709
889
  text =
710
- `Harness: "${failing.command}" has now failed 3 times with the same error. Stop and reflect ` +
711
- `before retrying. Write a report to "${reflectionReportPath(stateDir)}" with sections ` +
712
- `## Goal, ## Failed Attempts, ## Verified Facts, ## Diagnosis, and ## Decision. ` +
713
- `The Decision must begin with RESUME: or ESCALATE:. Include reflection token: ${trip.reflectionToken}. ` +
714
- `After the report is recorded, resume with a different approach. Failure: ${failing.error || "(no output captured)"}`
715
- recordCompliance(complianceFile, (c) => {
716
- c.circuitBreakerTrips++
717
- c.reflectionsForced++
718
- })
890
+ `Harness: verification did not complete - "${primary.command}" ${detail}. ` +
891
+ `No failing check was confirmed, so this does NOT count toward the repeated-failure reflection. ` +
892
+ `Diagnostics: ${primary.error || "(no output captured)"}. ` +
893
+ `Fix the cause (or raise the budget) and verify again; a new edit re-runs verification.`
719
894
  } else {
720
- text =
721
- `Harness: verification failed (${trip.remaining} retries before a forced reflection). ` +
722
- `"${failing.command}" - ${failing.error || "(no output captured)"}. Fix it and this will re-run automatically.`
895
+ const signature = buildFailureSignature(confirmed)
896
+ const trip = tripBreaker(breakerFile, signature)
897
+ if (trip.action === "force_reflection") {
898
+ const { stateDir } = pathsFor(activeSession)
899
+ text =
900
+ `Harness: "${confirmed.command}" has now failed 3 times with the same error. Stop and reflect ` +
901
+ `before retrying. Write a report to "${reflectionReportPath(stateDir)}" with sections ` +
902
+ `## Goal, ## Failed Attempts, ## Verified Facts, ## Diagnosis, and ## Decision. ` +
903
+ `The Decision must begin with RESUME: or ESCALATE:. Include reflection token: ${trip.reflectionToken}. ` +
904
+ `After the report is recorded, resume with a different approach. Failure: ${confirmed.error || "(no output captured)"}`
905
+ recordCompliance(complianceFile, (c) => {
906
+ c.circuitBreakerTrips++
907
+ c.reflectionsForced++
908
+ })
909
+ } else {
910
+ text =
911
+ `Harness: verification failed (${trip.remaining} retries before a forced reflection). ` +
912
+ `"${confirmed.command}" - ${confirmed.error || "(no output captured)"}. Fix it and this will re-run automatically.`
913
+ }
723
914
  }
724
915
 
725
916
  try {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "harness-enforcement",
3
3
  "description": "Repository inventory for the opencode enforcement plugin and its standalone helper",
4
- "version": "0.35.0",
4
+ "version": "0.36.1",
5
5
  "runtime": "index.mjs",
6
6
  "hooks": {
7
7
  "verify": "hooks/verify.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.35.0",
3
+ "version": "0.36.1",
4
4
  "description": "Harness OS: An Orchestrated Agent Operating System for AI-driven Development",
5
5
  "main": "harness-everything/scripts/bootstrap.js",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "scripts": {
10
10
  "test": "node self-evolve/scripts/self-regression.js",
11
11
  "self-regression": "node self-evolve/scripts/self-regression.js",
12
+ "verify:all": "node scripts/verify-all.js",
12
13
  "test:mechanism": "node ci/mechanism-test.js",
13
14
  "test:opencode:loadability": "node ci/mechanism-30-opencode-plugin-loadability.test.js",
14
15
  "test:mutations": "node ci/mutation-check.js",
@@ -20,6 +21,7 @@
20
21
  "test:consistency": "node ci/consistency-check.js && node ci/doc-capability-consistency.test.js && node ci/repository-contract.test.js && node ci/mechanism-2ao-plugin-sync.test.js && node ci/prompt-contract-regressions.test.js",
21
22
  "test:docs:capabilities": "node ci/doc-capability-consistency.test.js",
22
23
  "test:references": "node ci/reference-check.js",
24
+ "test:portability": "node ci/portability-lint.js",
23
25
  "test:release": "node ci/release-consistency-check.js && node ci/release-consistency-check.test.js && node ci/mechanism-2x-release-automation.test.js && node ci/mechanism-2x-release-commit-message.test.js && node ci/mechanism-2ap-sbom.test.js",
24
26
  "test:release:semantics": "node ci/semantic-release-analyzer.test.mjs",
25
27
  "test:install": "node scripts/verify-install.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harness-everything",
3
- "version": "0.35.0",
3
+ "version": "0.36.1",
4
4
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
5
5
  "author": {
6
6
  "name": "Miya Daniel",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "harness-everything",
4
- "version": "0.35.0",
4
+ "version": "0.36.1",
5
5
  "description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
6
6
  "author": {
7
7
  "name": "Miya Daniel",
@@ -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.35.0
7
+ version: 0.36.1
8
8
  ---
9
9
 
10
10
  # Harness Everything