harness-everything 0.16.0 → 0.16.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.
Files changed (96) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +2 -2
  4. package/CHANGELOG.md +8 -0
  5. package/README.md +7 -7
  6. package/VERIFICATION.md +3 -3
  7. package/ci/invariant-routing.test.js +58 -158
  8. package/ci/mechanism-2a-rule-of-3.test.js +73 -138
  9. package/ci/mechanism-2ac-atomic-commit-check.test.js +2 -2
  10. package/ci/mechanism-2af-worktree-isolation.test.js +38 -196
  11. package/ci/mechanism-2ah-workflow-lifecycle.test.js +20 -831
  12. package/ci/mechanism-2ai-workspace-fingerprint-batch.test.js +3 -4
  13. package/ci/mechanism-2b-boundary-guard.test.js +6 -16
  14. package/ci/mechanism-2e-scope-guard.test.js +8 -24
  15. package/ci/mechanism-2f-stop-gate.test.js +21 -75
  16. package/ci/mechanism-2n-opencode-plugin.test.js +13 -28
  17. package/ci/openai-plugin-package.test.js +8 -8
  18. package/ci/openai-plugin-runtime.test.js +2 -2
  19. package/ci/router-workflow-plan.test.js +6 -6
  20. package/ci/self-evolve-loop.test.js +4 -1
  21. package/ci/workflow-execution-contract.test.js +51 -129
  22. package/ci/workflow-plan-consumers-edge.test.js +1 -1
  23. package/ci/workflow-plan-consumers.test.js +6 -6
  24. package/docs/architecture.md +21 -21
  25. package/docs/enforcement-locks.md +41 -102
  26. package/docs/mechanism-first-skill-mesh.md +2 -2
  27. package/docs/philosophy.md +2 -2
  28. package/docs/platform-capabilities.md +3 -3
  29. package/docs/routing.md +14 -14
  30. package/docs/workflow-runtime.md +80 -110
  31. package/evals/harness-everything/tasks/positive-tier1.yaml +1 -1
  32. package/evals/harness-everything/tasks/positive-tier2.yaml +1 -1
  33. package/evals/harness-everything/tasks/positive-tier3.yaml +1 -1
  34. package/evals/using-git-worktrees/tasks/positive-trigger.yaml +1 -1
  35. package/fable-mode/SKILL.md +1 -1
  36. package/fable-mode/execution-guardrails/SKILL.md +1 -1
  37. package/fable-mode/fable-haiku/SKILL.md +1 -1
  38. package/fable-mode/fable-opus/SKILL.md +1 -1
  39. package/fable-mode/fable-sonnet/SKILL.md +1 -1
  40. package/fable-mode/scripts/workflow-plan-consumer.js +4 -3
  41. package/harness-everything/SKILL.md +11 -11
  42. package/harness-everything/references/triage-and-tiers.md +11 -11
  43. package/harness-everything/scripts/kernel-router-core.js +5 -5
  44. package/harness-everything/scripts/kernel-router.js +15 -17
  45. package/harness-everything/scripts/router-contract-core.js +1 -1
  46. package/hooks/hooks.json +10 -23
  47. package/hooks/scripts/atomic-commit-check.js +1 -1
  48. package/hooks/scripts/boundary-guard.js +4 -4
  49. package/hooks/scripts/contract-test.js +8 -11
  50. package/hooks/scripts/depth-guard.js +2 -2
  51. package/hooks/scripts/lib/workflow-runtime.js +24 -402
  52. package/hooks/scripts/rule-of-3-tracker.js +28 -31
  53. package/hooks/scripts/rule-of-3.js +4 -28
  54. package/hooks/scripts/state-persist.js +36 -136
  55. package/hooks/scripts/stop-gate.js +15 -48
  56. package/hooks/scripts/subagent-scope-guard.js +8 -8
  57. package/hooks/scripts/workflow-disposition.js +2 -10
  58. package/hooks/scripts/workflow-gate.js +33 -84
  59. package/hooks/scripts/workflow-mutation-denied.js +3 -15
  60. package/hooks/scripts/workflow-stop-gate.js +23 -38
  61. package/opencode-plugin/README.md +9 -10
  62. package/opencode-plugin/index.mjs +14 -30
  63. package/opencode-plugin/plugin.json +1 -1
  64. package/package.json +1 -1
  65. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  66. package/plugins/harness-everything/hooks/hooks.json +3 -3
  67. package/plugins/harness-everything/hooks/scripts/atomic-commit-check.js +1 -1
  68. package/plugins/harness-everything/hooks/scripts/boundary-guard.js +4 -4
  69. package/plugins/harness-everything/hooks/scripts/contract-test.js +8 -11
  70. package/plugins/harness-everything/hooks/scripts/depth-guard.js +2 -2
  71. package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +24 -402
  72. package/plugins/harness-everything/hooks/scripts/rule-of-3-tracker.js +28 -31
  73. package/plugins/harness-everything/hooks/scripts/rule-of-3.js +4 -28
  74. package/plugins/harness-everything/hooks/scripts/state-persist.js +36 -136
  75. package/plugins/harness-everything/hooks/scripts/stop-gate.js +15 -48
  76. package/plugins/harness-everything/hooks/scripts/subagent-scope-guard.js +8 -8
  77. package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +2 -10
  78. package/plugins/harness-everything/hooks/scripts/workflow-gate.js +33 -84
  79. package/plugins/harness-everything/hooks/scripts/workflow-mutation-denied.js +3 -15
  80. package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +23 -38
  81. package/plugins/harness-everything/plugin.json +1 -1
  82. package/plugins/harness-everything/scripts/lib/advisory-text.js +16 -29
  83. package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
  84. package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
  85. package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
  86. package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
  87. package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
  88. package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +4 -3
  89. package/plugins/harness-everything/skills/harness-everything/SKILL.md +11 -11
  90. package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +11 -11
  91. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +5 -5
  92. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +15 -17
  93. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +1 -1
  94. package/plugins/harness-everything/skills/using-git-worktrees/SKILL.md +5 -5
  95. package/scripts/lib/advisory-text.js +16 -29
  96. package/using-git-worktrees/SKILL.md +5 -5
@@ -202,10 +202,9 @@ try {
202
202
  const workflowFile = path.join(state.getSessionDir(repo, sessionId), 'workflow-run.json');
203
203
  const workflow = JSON.parse(fs.readFileSync(workflowFile, 'utf8'));
204
204
  check(workflow.state === 'running' &&
205
- workflow.budget?.counters?.iterations === 1 &&
206
- workflow.lastMutationAt > 0 &&
207
- workflow.budget?.events?.at(-1)?.evidence === 'shell:fingerprint-limit',
208
- '#169 fingerprint-limit fallback records one auditable conservative mutation');
205
+ !workflow.budget?.counters &&
206
+ !fs.existsSync(path.join(state.getSessionDir(repo, sessionId), 'mutation-probes')),
207
+ '#190 shell admission no longer creates conservative budget/probe state');
209
208
 
210
209
  console.log('PASS: batched workspace fingerprint (' + passed + ' assertions, 1000-file ' + elapsedMs.toFixed(1) + 'ms)');
211
210
  } finally {
@@ -1,16 +1,6 @@
1
- const fs = require('fs');
2
- const helper = require('./test-helper');
3
-
4
- console.log('\n[2b] Boundary guard...');
5
-
6
- const bigFile = helper.tempFile('.mechanism-test-big.tmp');
7
- fs.writeFileSync(bigFile, 'x'.repeat(600 * 1024));
8
- const boundaryResult = helper.runHook('boundary-guard.js', { tool_name: 'Read', tool_input: { file_path: bigFile } });
9
- helper.check(
10
- '2b. Blocks a 600KB Read without limit/offset (exit=2, BLOCKED)',
11
- boundaryResult.code === 2 && boundaryResult.stderr.includes('[Boundary Guard] BLOCKED'),
12
- `Got exit=${boundaryResult.code}, stderr="${boundaryResult.stderr.slice(0, 200)}"`
13
- );
14
- fs.unlinkSync(bigFile);
15
-
16
- helper.finish();
1
+ const fs=require('fs'),path=require('path'),{spawnSync}=require('child_process'),helper=require('./test-helper');
2
+ console.log('\n[2b] Boundary reminder...');
3
+ const bigFile=helper.tempFile('.mechanism-test-big.tmp');fs.writeFileSync(bigFile,'x'.repeat(600*1024));
4
+ const result=spawnSync(process.execPath,[path.join(helper.hooksDir,'boundary-guard.js')],{input:JSON.stringify({tool_name:'Read',tool_input:{file_path:bigFile}}),encoding:'utf8'});
5
+ helper.check('2b. Large read emits a reminder but remains available',result.status===0&&/Boundary Reminder/i.test(result.stderr),`exit=${result.status}, stderr=${result.stderr.slice(0,200)}`);
6
+ fs.unlinkSync(bigFile);helper.finish();
@@ -1,24 +1,8 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const helper = require('./test-helper');
4
-
5
- console.log('\n[2e] Subagent scope guard...');
6
-
7
- helper.runHook('subagent-scope-guard.js', {
8
- session_id: helper.SESSION_ID, tool_name: 'Task', hook_event_name: 'PreToolUse', tool_input: {},
9
- });
10
- const scopeFile = helper.tempFile('.mechanism-test-scope.tmp');
11
- fs.writeFileSync(scopeFile, 'unexpected change');
12
- const scopeResult = helper.runHook('subagent-scope-guard.js', {
13
- session_id: helper.SESSION_ID, tool_name: 'Task', hook_event_name: 'PostToolUse', tool_input: {},
14
- });
15
- helper.check(
16
- '2e. Flags the newly-created file after a Task burst (exit=2, lists filename)',
17
- scopeResult.code === 2 &&
18
- scopeResult.stderr.includes('Subagent Scope Guard') &&
19
- scopeResult.stderr.includes(path.basename(scopeFile)),
20
- `Got exit=${scopeResult.code}, stderr="${scopeResult.stderr.slice(0, 300)}"`
21
- );
22
- fs.unlinkSync(scopeFile);
23
-
24
- helper.finish();
1
+ const fs=require('fs'),path=require('path'),{spawnSync}=require('child_process'),helper=require('./test-helper');
2
+ console.log('\n[2e] Subagent scope reminder...');
3
+ function run(payload){return spawnSync(process.execPath,[path.join(helper.hooksDir,'subagent-scope-guard.js')],{input:JSON.stringify(payload),encoding:'utf8'});}
4
+ run({session_id:helper.SESSION_ID,tool_name:'Task',hook_event_name:'PreToolUse',tool_input:{}});
5
+ const scopeFile=helper.tempFile('.mechanism-test-scope.tmp');fs.writeFileSync(scopeFile,'unexpected change');
6
+ const result=run({session_id:helper.SESSION_ID,tool_name:'Task',hook_event_name:'PostToolUse',tool_input:{}});
7
+ helper.check('2e. Scope drift is reported without blocking',result.status===0&&result.stderr.includes(path.basename(scopeFile)),`exit=${result.status}, stderr=${result.stderr.slice(0,300)}`);
8
+ fs.unlinkSync(scopeFile);helper.finish();
@@ -1,76 +1,22 @@
1
- const fs = require('fs');
2
- const helper = require('./test-helper');
3
-
4
- console.log('\n[2f] Stop gate...');
5
-
6
- helper.writeState('handoff-state.json', { status: 'idle', lastEditAt: Date.now(), lastVerifyAt: 0 });
7
- const dirtyFile = helper.tempFile('.mechanism-test-dirty.tmp');
8
- fs.writeFileSync(dirtyFile, 'dirty');
9
- const firstStop = helper.runHook('stop-gate.js', { session_id: helper.SESSION_ID });
10
- helper.check(
11
- '2f. First stop after an unverified edit bounces (exit=2)',
12
- firstStop.code === 2 && firstStop.stderr.includes('[Stop Gate]'),
13
- `Got exit=${firstStop.code}, stderr="${firstStop.stderr.slice(0, 200)}"`
14
- );
15
- const secondStop = helper.runHook('stop-gate.js', { session_id: helper.SESSION_ID });
16
- helper.check(
17
- '2f. Same edit batch does not bounce twice (exit=0)',
18
- secondStop.code === 0,
19
- `Got exit=${secondStop.code}`
20
- );
21
- fs.unlinkSync(dirtyFile);
22
-
23
- console.log('\n[2f] workflow-stop-gate (iterative-single) verification signal...');
24
-
25
- const now = Date.now();
26
-
27
- // Bug reproduction (issue #153): a verify-classified command that genuinely
28
- // succeeded but whose host never reported a numeric exit code (observed on
29
- // at least one live host - state-persist.js writes lastVerifyExitCode: null
30
- // for any non-failing command when the PostToolUse payload has no numeric
31
- // exitCode) must not be treated as unverified.
32
- helper.writeState('workflow-run.json', {
33
- schemaVersion: 1, sessionId: helper.SESSION_ID, strategy: 'iterative-single',
34
- state: 'running', lastMutationAt: now - 5000,
35
- });
36
- helper.writeState('handoff-state.json', {
37
- status: 'idle', lastEditAt: now - 5000, lastVerifyAt: now, lastVerifyExitCode: null,
38
- });
39
- const hostExitCodeUnavailable = helper.runHook('workflow-stop-gate.js', { session_id: helper.SESSION_ID });
40
- helper.check(
41
- '2f. workflow-stop-gate resolves when verify ran after edit even without a numeric exit code',
42
- hostExitCodeUnavailable.code === 0 && helper.readState('workflow-run.json').state === 'satisfied',
43
- `Got exit=${hostExitCodeUnavailable.code}, stderr="${hostExitCodeUnavailable.stderr.slice(0, 300)}"`
44
- );
45
-
46
- // Regression guard: explicit exit 0 (host does report it) must still resolve.
47
- helper.writeState('workflow-run.json', {
48
- schemaVersion: 1, sessionId: helper.SESSION_ID, strategy: 'iterative-single',
49
- state: 'running', lastMutationAt: now - 5000,
50
- });
51
- helper.writeState('handoff-state.json', {
52
- status: 'idle', lastEditAt: now - 5000, lastVerifyAt: now, lastVerifyExitCode: 0,
53
- });
54
- const hostExitCodeReported = helper.runHook('workflow-stop-gate.js', { session_id: helper.SESSION_ID });
55
- helper.check(
56
- '2f. workflow-stop-gate still resolves when the host does report exit code 0',
57
- hostExitCodeReported.code === 0 && helper.readState('workflow-run.json').state === 'satisfied',
58
- `Got exit=${hostExitCodeReported.code}`
59
- );
60
-
61
- // Negative control: no verification ran after the edit at all -> stays blocked.
62
- helper.writeState('workflow-run.json', {
63
- schemaVersion: 1, sessionId: helper.SESSION_ID, strategy: 'iterative-single',
64
- state: 'running', lastMutationAt: now,
65
- });
66
- helper.writeState('handoff-state.json', {
67
- status: 'idle', lastEditAt: now, lastVerifyAt: now - 5000, lastVerifyExitCode: null,
68
- });
69
- const staleVerify = helper.runHook('workflow-stop-gate.js', { session_id: helper.SESSION_ID });
70
- helper.check(
71
- '2f. workflow-stop-gate still blocks when no verification ran after the edit',
72
- staleVerify.code === 2 && staleVerify.stderr.includes('verification-after-edit-missing'),
73
- `Got exit=${staleVerify.code}, stderr="${staleVerify.stderr.slice(0, 300)}"`
74
- );
75
-
1
+ const fs=require('fs'),path=require('path'),{spawnSync}=require('child_process'),helper=require('./test-helper');
2
+ console.log('\n[2f] Verification reminders...');
3
+ function run(script,payload){return spawnSync(process.execPath,[path.join(helper.hooksDir,script)],{input:JSON.stringify(payload),encoding:'utf8'});}
4
+ helper.writeState('handoff-state.json',{status:'idle',lastEditAt:Date.now(),lastVerifyAt:0});
5
+ const dirty=helper.tempFile('.mechanism-test-dirty.tmp');fs.writeFileSync(dirty,'dirty');
6
+ const reminder=run('stop-gate.js',{session_id:helper.SESSION_ID});
7
+ helper.check('2f. Unverified dirty Stop emits reminder and exits 0',reminder.status===0&&/Verification Reminder/i.test(reminder.stderr),`exit=${reminder.status}, stderr=${reminder.stderr.slice(0,200)}`);
8
+ helper.check('2f. Stop reminder creates no lock/throttle state',!helper.stateExists('stop-gate-state.json'),'unexpected stop-gate-state.json');
9
+ fs.unlinkSync(dirty);
10
+
11
+ const now=Date.now();
12
+ helper.writeState('workflow-run.json',{schemaVersion:1,sessionId:helper.SESSION_ID,strategy:'iterative-single',state:'running',lastMutationAt:now-5000});
13
+ helper.writeState('handoff-state.json',{status:'idle',lastEditAt:now-5000,lastVerifyAt:now,lastVerifyExitCode:null});
14
+ const verified=run('workflow-stop-gate.js',{session_id:helper.SESSION_ID});
15
+ helper.check('2f. Observed verification can mark workflow satisfied',verified.status===0&&helper.readState('workflow-run.json').state==='satisfied',verified.stderr);
16
+
17
+ helper.writeState('workflow-run.json',{schemaVersion:1,sessionId:helper.SESSION_ID,strategy:'iterative-single',state:'running',lastMutationAt:now});
18
+ helper.writeState('handoff-state.json',{status:'idle',lastEditAt:now,lastVerifyAt:now-5000,lastVerifyExitCode:null});
19
+ const stale=run('workflow-stop-gate.js',{session_id:helper.SESSION_ID});
20
+ helper.check('2f. Missing verification is reminder-only',stale.status===0&&/verification-after-edit-missing/i.test(stale.stderr),`exit=${stale.status}, stderr=${stale.stderr.slice(0,300)}`);
21
+ helper.check('2f. Missing verification does not create blocked state',helper.readState('workflow-run.json').state==='running',JSON.stringify(helper.readState('workflow-run.json')));
76
22
  helper.finish();
@@ -158,7 +158,7 @@ helper.check(
158
158
  const sig = Object.keys(breaker.failures)[0];
159
159
  helper.check(
160
160
  '2n. two failures on the same signature: still allowed, not yet tripped',
161
- breaker.failures[sig].count === 2 && breaker.hardLock === false,
161
+ breaker.failures[sig].count === 2 && breaker.hardLock === undefined,
162
162
  JSON.stringify(breaker)
163
163
  );
164
164
  helper.check('2n. each failed verification sends exactly one follow-up prompt', promptCalls.length === 2, `${promptCalls.length} prompt(s) sent`);
@@ -235,8 +235,8 @@ helper.check(
235
235
  );
236
236
  breaker = JSON.parse(fs.readFileSync(breakerFile, 'utf8'));
237
237
  helper.check(
238
- '2n. apply_patch reflection completes the lifecycle and persists lastReflection',
239
- Number.isFinite(breaker.lastReflection) && breaker.lastReflection > 0 && breaker.reflectionPending === false,
238
+ '2n. apply_patch reflection completes the lifecycle, resets the signature count, and persists lastReflection',
239
+ Number.isFinite(breaker.lastReflection) && breaker.lastReflection > 0 && breaker.reflectionPending === false && breaker.failures[sig].count === 0,
240
240
  JSON.stringify(breaker)
241
241
  );
242
242
 
@@ -330,42 +330,27 @@ helper.check(
330
330
  }
331
331
  helper.check('2n. mixed apply_patch cannot bypass a pending reflection', mixedPatchBlocked, 'unrelated files were accepted with the reflection artifact');
332
332
 
333
- // A new code edit after a completed reflection is the post-reflection retry.
333
+ // A fresh failure after reflection starts a new three-failure cycle.
334
334
  await hooks['tool.execute.after']({ tool: 'edit', sessionID: 's1', callID: 'c5' }, { title: '', output: '', metadata: {} });
335
335
  await hooks.event({ event: { type: 'session.idle', properties: { sessionID: 's1' } } });
336
336
  breaker = JSON.parse(fs.readFileSync(breakerFile, 'utf8'));
337
- helper.check('2n. the same failure returning after a reflection hard-locks the breaker', breaker.hardLock === true, JSON.stringify(breaker));
338
- helper.check(
339
- '2n. the hard-lock prompt says locked',
340
- /lock/i.test(promptCalls[promptCalls.length - 1].body.parts[0].text),
341
- promptCalls[promptCalls.length - 1].body.parts[0].text
342
- );
343
-
344
- // --- circuit breaker enforcement (tool.execute.before) ---
345
- let blocked = false;
337
+ helper.check('2n. first post-reflection failure starts again at count=1', breaker.failures[sig].count === 1 && breaker.reflectionPending === false, JSON.stringify(breaker));
338
+ let postReflectionEditAllowed = true;
346
339
  try {
347
340
  await hooks['tool.execute.before']({ tool: 'edit', sessionID: 's1', callID: 'c6' }, { args: {} });
348
341
  } catch {
349
- blocked = true;
350
- }
351
- helper.check('2n. hard-locked breaker throws on the next edit attempt', blocked, 'tool.execute.before did not throw');
352
-
353
- let readBlocked = false;
354
- try {
355
- await hooks['tool.execute.before']({ tool: 'read', sessionID: 's1', callID: 'c7' }, { args: {} });
356
- } catch {
357
- readBlocked = true;
342
+ postReflectionEditAllowed = false;
358
343
  }
359
- helper.check('2n. a hard lock only blocks edit-shaped tools, not read', !readBlocked, 'tool.execute.before blocked a read');
344
+ helper.check('2n. no permanent post-reflection hard lock remains', postReflectionEditAllowed, 'tool.execute.before unexpectedly blocked after one fresh failure');
360
345
 
361
346
  // The same workspace can host independent sessions. Session s2 starts with
362
- // a fresh breaker and cannot inherit s1's hard lock.
347
+ // a fresh breaker and cannot inherit s1 state.
363
348
  await hooks['tool.execute.after']({ tool: 'edit', sessionID: 's2', callID: 's2-c1', args: {} }, { title: '', output: '', metadata: {} });
364
349
  await hooks.event({ event: { type: 'session.idle', properties: { sessionID: 's2' } } });
365
350
  const secondBreaker = JSON.parse(fs.readFileSync(path.join(secondSessionDir, 'circuit-breaker.json'), 'utf8'));
366
351
  helper.check(
367
352
  '2n. a second session has an independent breaker stream',
368
- secondBreaker.hardLock === false && Object.values(secondBreaker.failures).some((entry) => entry.count === 1),
353
+ secondBreaker.hardLock === undefined && Object.values(secondBreaker.failures).some((entry) => entry.count === 1),
369
354
  JSON.stringify(secondBreaker)
370
355
  );
371
356
 
@@ -382,9 +367,9 @@ helper.check(
382
367
  corruptStateMessage = String(error && error.message ? error.message : error);
383
368
  }
384
369
  helper.check(
385
- '2n. corrupt breaker state fails closed before an edit',
386
- corruptStateBlocked && /corrupt/i.test(corruptStateMessage),
387
- corruptStateMessage || 'tool.execute.before allowed an edit with corrupt breaker state'
370
+ '2n. corrupt breaker state fails open instead of becoming a cognitive lock',
371
+ !corruptStateBlocked,
372
+ corruptStateMessage || 'corrupt state was ignored as expected'
388
373
  );
389
374
 
390
375
  // A prompt transport failure is retryable without re-running verification
@@ -162,8 +162,8 @@ assert.ok(hooks.hooks.Stop?.length, 'Stop hook missing');
162
162
  const hookText = JSON.stringify(hooks);
163
163
  assert.match(hookText, /PLUGIN_ROOT/, 'plugin hooks must resolve from PLUGIN_ROOT');
164
164
  assert.match(hookText, /commandWindows/, 'plugin hooks need a Windows command override');
165
- assert.match(hookText, /workflow-gate\.js/, 'plugin hooks must package workflow bypass enforcement');
166
- assert.match(hookText, /workflow-stop-gate\.js/, 'plugin hooks must package workflow completion enforcement');
165
+ assert.match(hookText, /workflow-gate\.js/, 'plugin hooks must package workflow guidance');
166
+ assert.match(hookText, /workflow-stop-gate\.js/, 'plugin hooks must package workflow completion reminders');
167
167
 
168
168
  const session = spawnSync(process.execPath, [path.join(PLUGIN, 'hooks', 'session-start.js')], { encoding: 'utf8' });
169
169
  assert.strictEqual(session.status, 0, session.stderr);
@@ -190,15 +190,15 @@ assert.strictEqual(second.status, 0, second.stderr);
190
190
  assert.strictEqual(first.stdout, second.stdout, 'identical routing input must produce identical output');
191
191
  assert.match(first.stdout, /RECOMMENDED TIER:\s*Tier 2/i);
192
192
  assert.match(first.stdout, /REQUIRED HARNESS INVARIANTS/);
193
- assert.match(first.stdout, /WORKFLOW SKILLS \(EVALUATE APPLICABILITY .*SELECTED WORKFLOW IS MANDATORY\)/);
193
+ assert.match(first.stdout, /WORKFLOW SKILLS \(EVALUATE APPLICABILITY .*SELECTED WORKFLOW IS GUIDANCE\)/);
194
194
  assert.match(first.stdout, /evaluate-suggestions-before-skip/);
195
195
  assert.match(first.stdout, /read its complete SKILL\.md entry/);
196
196
  assert.doesNotMatch(first.stdout, /BASE EXECUTION LOOP/);
197
- assert.match(first.stdout, /ORCHESTRATION POLICY: Mandatory applicable workflow/);
198
- assert.match(first.stdout, /WORKFLOW EXECUTION CONTRACT \(MANDATORY WHEN SELECTED\)/);
197
+ assert.match(first.stdout, /ORCHESTRATION POLICY: Selected topology is planning guidance/);
198
+ assert.match(first.stdout, /WORKFLOW GUIDANCE \(ADVISORY WHEN SELECTED\)/);
199
199
  assert.match(first.stdout, /Selected workflow: iterative-single/);
200
- assert.match(first.stdout, /Completion requires the workflow obligations and objective verification/);
201
- assert.doesNotMatch(first.stdout, /execution remains advisory after evaluation/);
200
+ assert.match(first.stdout, /Before claiming completion, review unresolved workflow evidence and objective verification/);
201
+ assert.doesNotMatch(first.stdout, /budget-exhausted|reset-budget/);
202
202
 
203
203
  const trivial = route('Update one README typo');
204
204
  assert.strictEqual(trivial.status, 0, trivial.stderr);
@@ -213,4 +213,4 @@ assert.match(unmatched.stdout, /RECOMMENDED TIER:\s*Unclassified/i);
213
213
  assert.match(unmatched.stdout, /"tier":"unclassified"/i);
214
214
  assert.doesNotMatch(unmatched.stdout, /RECOMMENDED TIER:\s*Tier 1/i);
215
215
 
216
- console.log(`OpenAI plugin package verified: ${skillNames.length} skills, publication metadata, hooks, marketplace, deterministic mandatory-workflow routing.`);
216
+ console.log(`OpenAI plugin package verified: ${skillNames.length} skills, publication metadata, hooks, marketplace, deterministic guidance-first routing.`);
@@ -169,7 +169,7 @@ const subagentStop = runHook('hooks/scripts/subagent-stop.js', {
169
169
  cwd: fixture,
170
170
  agent_type: 'worker'
171
171
  }, stateHome);
172
- assert.strictEqual(subagentStop.status, 2, subagentStop.stderr);
173
- assert.match(subagentStop.stderr, /Subagent Scope Guard/);
172
+ assert.strictEqual(subagentStop.status, 0, subagentStop.stderr);
173
+ assert.match(subagentStop.stderr, /Subagent Scope Reminder/);
174
174
 
175
175
  console.log('OpenAI plugin runtime compatibility verified.');
@@ -75,12 +75,12 @@ if (validContract(iterative, 'iterative-single')) {
75
75
  const plan = iterative.contract.workflowPlan;
76
76
  check(plan.tier === 'tier2', 'ordinary test-first bug fix is tier2');
77
77
  check(plan.strategy === 'iterative-single', 'ordinary bug fix selects iterative-single');
78
- check(plan.limits.maxIterations === ITERATIVE_MAX_ITERATIONS, 'iterative-single has an explicit loop budget');
79
- check(plan.limits.maxRevisionRounds === WORKFLOW_MAX_REVISION_ROUNDS, 'workflow exposes one numeric revision budget');
80
- check(plan.limits.maxReplans === FABLE_MAX_REPLANS, 'workflow exposes one numeric replan budget');
81
- check(plan.limits.maxWorkers === FABLE_MAX_WORKERS, 'workflow exposes one numeric worker cap');
78
+ check(plan.limits.maxIterations === ITERATIVE_MAX_ITERATIONS, 'iterative-single retains an advisory iteration guidance value');
79
+ check(plan.limits.maxRevisionRounds === WORKFLOW_MAX_REVISION_ROUNDS, 'workflow exposes an advisory revision guidance value');
80
+ check(plan.limits.maxReplans === FABLE_MAX_REPLANS, 'workflow exposes an advisory replan guidance value');
81
+ check(plan.limits.maxWorkers === FABLE_MAX_WORKERS, 'workflow exposes an advisory worker guidance value');
82
82
  check(plan.requiredInvariants.includes('objective-verification'), 'iterative-single carries objective verification invariant');
83
- check(plan.requiredInvariants.includes('loop-budget'), 'iterative-single carries loop-budget invariant');
83
+ check(plan.requiredInvariants.includes('loop-awareness'), 'iterative-single carries loop-awareness guidance');
84
84
  check(plan.suggestedSkills.includes('tdd'), 'TDD stays advisory rather than an invariant');
85
85
  check(plan.memory.write === 'none', 'ordinary iterative work cannot write durable memory');
86
86
  check(!plan.requiredInvariants.includes('tdd'), 'suggested skills are separated from mandatory invariants');
@@ -267,7 +267,7 @@ const kernelIterative = spawnSync(process.execPath, [kernelRouter, 'Fix this che
267
267
  encoding: 'utf8',
268
268
  });
269
269
  check(kernelIterative.stdout.includes('"strategy":"iterative-single"'), 'kernel consumes strategy from structured plan');
270
- check(kernelIterative.stdout.includes('loop-budget:'), 'kernel prints dynamic plan invariants');
270
+ check(kernelIterative.stdout.includes('loop-awareness:'), 'kernel prints dynamic plan guidance');
271
271
  check(kernelIterative.stdout.includes(' - tdd'), 'kernel prints advisory skills separately');
272
272
 
273
273
  for (const schemaPath of [
@@ -168,7 +168,10 @@ try {
168
168
  tool_use_id: 'verify-fail-1',
169
169
  tool_response: { exitCode: 1, stderr: 'fixture assertion failed' },
170
170
  });
171
- check(firstFail.status === 2, 'first verifier failure remains authoritative and blocks stage acceptance');
171
+ check(firstFail.status === 0, 'first verifier failure remains authoritative without blocking the hook');
172
+
173
+ const failedContract = JSON.parse(fs.readFileSync(path.join(prepared.runRoot, 'contracts', 'verify-recovery.json'), 'utf8'));
174
+ check(failedContract.status === 'fail', 'failed verifier evidence remains authoritative in the stage contract');
172
175
 
173
176
  const laterPass = run(contractHook, {
174
177
  ...basePayload,
@@ -1,133 +1,55 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
-
4
- const fs = require('fs');
5
- const os = require('os');
6
- const path = require('path');
7
- const { spawnSync } = require('child_process');
8
-
3
+ const fs = require('fs'), os = require('os'), path = require('path'), { spawnSync } = require('child_process');
9
4
  const ROOT = path.resolve(__dirname, '..');
10
- const { recordBudgetEvent, resetWorkflowBudget } = require(path.join(ROOT, 'hooks/scripts/lib/workflow-runtime.js'));
11
- let failed = 0;
12
-
13
- function check(condition, message) {
14
- if (condition) console.log(` PASS ${message}`);
15
- else { console.error(` FAIL ${message}`); failed++; }
16
- }
17
-
18
- function runNode(args, options = {}) {
19
- return spawnSync(process.execPath, args, { cwd: ROOT, encoding: 'utf8', ...options });
20
- }
21
-
22
- console.log('=== Workflow Execution Enforcement ===');
23
-
24
- for (const rel of [
25
- 'harness-everything/scripts/kernel-router.js',
26
- 'harness-everything/scripts/kernel-router-core.js',
27
- 'hooks/scripts/workflow-gate.js',
28
- 'hooks/scripts/workflow-stop-gate.js',
29
- 'hooks/scripts/workflow-disposition.js',
30
- ]) {
31
- const result = runNode(['--check', path.join(ROOT, rel)]);
32
- check(result.status === 0, `${rel} parses as valid JavaScript`);
33
- }
34
-
35
- const routed = runNode([
36
- path.join(ROOT, 'harness-everything/scripts/kernel-router.js'),
37
- 'audit the entire repository architecture and coordinate multiple modules',
38
- ]);
39
- check(routed.status === 0, 'enforced kernel route exits successfully');
40
- check(routed.stdout.includes('"strategy":"fable-staged"'), 'macro prompt selects fable-staged');
41
- check(routed.stdout.includes('WORKFLOW EXECUTION CONTRACT (MANDATORY WHEN SELECTED)'), 'wrapper emits mandatory execution contract');
42
- check(routed.stdout.includes('Execute the selected workflow to resolution'), 'wrapper requires workflow resolution');
43
- check(routed.stdout.includes('workflow-uncovered-scope'), 'wrapper exposes only explicit evidence-backed escape path');
44
- check(!routed.stdout.includes('execution remains advisory after evaluation'), 'wrapper removes advisory-execution runtime wording');
45
-
46
- const disposition = fs.readFileSync(path.join(ROOT, 'hooks/scripts/workflow-disposition.js'), 'utf8');
47
- check(disposition.includes("'workflow-uncovered-scope'"), 'escape supports workflow-uncovered-scope');
48
- check(disposition.includes("'host-capability-unavailable'"), 'escape supports host-capability-unavailable');
49
- check(disposition.includes('generic simple/routine/already-clear reasons are intentionally rejected'), 'generic confidence-based escape reasons are rejected');
50
- check(disposition.includes('--scope is required') && disposition.includes('--evidence is required') && disposition.includes('--stage-id must name'), 'escape requires a declared stage, scope and evidence');
51
-
52
- const hooks = JSON.parse(fs.readFileSync(path.join(ROOT, 'hooks/hooks.json'), 'utf8'));
53
- check((hooks.hooks.PreToolUse || []).some(entry => entry.id === 'harness:pre:workflow-gate'), 'Claude hook wiring includes pre-mutation workflow gate');
54
- check((hooks.hooks.Stop || []).some(entry => entry.id === 'harness:stop:workflow-completion-gate'), 'Claude hook wiring includes workflow completion gate');
55
-
56
-
57
- const budgetRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-budget-'));
58
- function budgetContext(name, strategy = 'iterative-single', limits = {}) {
59
- const file = path.join(budgetRoot, name, 'workflow-run.json');
60
- return {
61
- root: path.join(budgetRoot, name),
62
- sessionId: name,
63
- sessionDir: path.join(budgetRoot, name),
64
- file,
65
- workflow: {
66
- schemaVersion: 2,
67
- sessionId: name,
68
- workflowId: `wf-${name}`,
69
- state: 'running',
70
- strategy,
71
- revision: 0,
72
- workflowPlan: {
73
- strategy,
74
- limits: { maxIterations: 8, maxRevisionRounds: 2, maxReplans: 2, maxWorkers: 4, ...limits },
75
- },
76
- },
77
- };
78
- }
79
- function expectBudgetBlock(fn, reason, message) {
80
- try {
81
- fn();
82
- check(false, message);
83
- } catch (error) {
84
- check(error.code === 'HARNESS_WORKFLOW_BUDGET' && error.message.includes(reason), `${message} (${error.message})`);
5
+ const runtime = require(path.join(ROOT, 'hooks/scripts/lib/workflow-runtime.js'));
6
+ let failed=0;
7
+ function check(c,m){if(c)console.log(' PASS '+m);else{console.error(' FAIL '+m);failed++;}}
8
+ function runNode(args){return spawnSync(process.execPath,args,{cwd:ROOT,encoding:'utf8'});}
9
+ console.log('=== Workflow Guidance Contract ===');
10
+ for(const rel of ['harness-everything/scripts/kernel-router.js','hooks/scripts/workflow-gate.js','hooks/scripts/workflow-stop-gate.js','hooks/scripts/workflow-disposition.js','hooks/scripts/state-persist.js','hooks/scripts/rule-of-3.js','hooks/scripts/rule-of-3-tracker.js'])check(runNode(['--check',path.join(ROOT,rel)]).status===0,rel+' parses as valid JavaScript');
11
+ const ctx={workflow:{workflowPlan:{limits:{maxIterations:8,maxRevisionRounds:2,maxReplans:2,maxWorkers:4}}}};
12
+ for(const type of ['iteration','revision','replan','worker-acquire'])check(runtime.recordBudgetEvent(ctx,type,{evidence:'fixture'}).advisory===true,type+' is advisory rather than blocking');
13
+ check(runtime.mutationProbeReservations(ctx)===0,'mutation probes reserve no capacity');
14
+ check(runtime.registerMutationProbe(ctx,'x','y')===null,'mutation probe registration is a no-op');
15
+ check(runtime.resetWorkflowBudget(ctx,'legacy').retired===true,'reset-budget API is retired compatibility only');
16
+ check(!runtime.WORKFLOW_CONTROLLER_COMMANDS.has('reset-budget'),'reset-budget is no longer a controller command');
17
+ const rs=fs.readFileSync(path.join(ROOT,'hooks/scripts/lib/workflow-runtime.js'),'utf8');
18
+ check(!rs.includes('.mutation-probes.lock'),'runtime contains no mutation-probe lock');
19
+ check(!rs.includes('budget-exhausted'),'runtime contains no budget-exhausted transition');
20
+ const gs=fs.readFileSync(path.join(ROOT,'hooks/scripts/workflow-gate.js'),'utf8');
21
+ check(!gs.includes('registerMutationProbe')&&!gs.includes('recordBudgetEvent'),'workflow gate has no probe/budget enforcement');
22
+ check(gs.includes('[Workflow Reminder]'),'workflow gate emits reminders');
23
+ const r3=fs.readFileSync(path.join(ROOT,'hooks/scripts/rule-of-3.js'),'utf8');
24
+ check(r3.includes('state.count >= 3'),'Rule of 3 uses fixed third-failure boundary');
25
+ check(!r3.includes('blockForHumanDecision'),'Rule of 3 has no second-stage permanent hard lock');
26
+ const tr=fs.readFileSync(path.join(ROOT,'hooks/scripts/rule-of-3-tracker.js'),'utf8');
27
+ check(!tr.includes('threshold = 2')&&!tr.includes('threshold = 4'),'Rule of 3 has no category-specific thresholds');
28
+ const oc=fs.readFileSync(path.join(ROOT,'opencode-plugin/index.mjs'),'utf8');
29
+ check(!oc.includes('action: "hard_lock"'),'OpenCode has no permanent hard-lock action');
30
+ const hk=JSON.parse(fs.readFileSync(path.join(ROOT,'hooks/hooks.json'),'utf8'));
31
+ check(!hk.hooks.PermissionDenied,'obsolete mutation-probe PermissionDenied hook is removed');
32
+
33
+ const freshnessRoot=fs.mkdtempSync(path.join(os.tmpdir(),'harness-fable-freshness-'));
34
+ try{
35
+ const contracts=path.join(freshnessRoot,'contracts'),evidence=path.join(freshnessRoot,'evidence');fs.mkdirSync(contracts);fs.mkdirSync(evidence);
36
+ const run={runId:'freshness-run',planId:'freshness-plan',sessionId:'freshness-session',stageIds:['worker','verifier']};
37
+ const workerAt=Date.now()-10000,verifierAt=Date.now()-5000;
38
+ const stages=[
39
+ {stageId:'worker',agent:'fable-worker',workerId:'worker-1',writeSet:['src/x.js'],dependsOn:[],checkCommand:'worker-check',verifiedAt:new Date(workerAt).toISOString()},
40
+ {stageId:'verifier',agent:'fable-verifier',workerId:'verifier-1',writeSet:[],dependsOn:['worker'],checkCommand:'verify-check',verifiedAt:new Date(verifierAt).toISOString()},
41
+ ];
42
+ for(const stage of stages){
43
+ const contract={...stage,runId:run.runId,planId:run.planId,sessionId:run.sessionId,status:'pass'};
44
+ fs.writeFileSync(path.join(contracts,stage.stageId+'.json'),JSON.stringify(contract));
45
+ fs.writeFileSync(path.join(evidence,stage.stageId+'.json'),JSON.stringify({status:'pass',exitCode:0,planId:run.planId,runId:run.runId,stageId:stage.stageId,sessionId:run.sessionId,checkCommand:stage.checkCommand,observedAt:stage.verifiedAt,workerId:stage.workerId}));
85
46
  }
86
- }
87
-
88
- const iterationCtx = budgetContext('iteration');
89
- for (let i = 0; i < 8; i++) recordBudgetEvent(iterationCtx, 'iteration', { evidence: `mutation-${i + 1}` });
90
- check(iterationCtx.workflow.budget.counters.iterations === 8, 'iteration budget allows exactly maxIterations events');
91
- expectBudgetBlock(() => recordBudgetEvent(iterationCtx, 'iteration', { evidence: 'mutation-9' }), 'iteration-budget-exhausted', 'ninth iterative event is blocked');
92
- check(iterationCtx.workflow.state === 'blocked' && iterationCtx.workflow.budget.state === 'budget-exhausted', 'iteration exhaustion is persisted as blocked evidence');
93
- const persistedIteration = JSON.parse(fs.readFileSync(iterationCtx.file, 'utf8'));
94
- check(persistedIteration.blockReason === 'iteration-budget-exhausted', 'budget exhaustion survives on-disk workflow state');
95
-
96
- const isolatedCtx = budgetContext('isolated');
97
- recordBudgetEvent(isolatedCtx, 'iteration', { evidence: 'independent-run' });
98
- check(isolatedCtx.workflow.budget.counters.iterations === 1, 'another run/session has independent accounting');
99
- check(iterationCtx.workflow.budget.counters.iterations === 8, 'independent accounting does not reset exhausted run');
100
-
101
- resetWorkflowBudget(iterationCtx, 'new explicitly authorized attempt');
102
- check(iterationCtx.workflow.state === 'pending' && iterationCtx.workflow.budget.epoch === 1, 'budget reset is explicit, auditable, and starts a new epoch');
103
- check(iterationCtx.workflow.budget.counters.iterations === 0 && iterationCtx.workflow.budget.resets.length === 1, 'reset clears only the target run counters and retains history');
104
-
105
- const revisionCtx = budgetContext('revision', 'fable-staged');
106
- recordBudgetEvent(revisionCtx, 'revision', { evidence: 'verifier-fail-1' });
107
- recordBudgetEvent(revisionCtx, 'revision', { evidence: 'verifier-fail-2' });
108
- expectBudgetBlock(() => recordBudgetEvent(revisionCtx, 'revision', { evidence: 'verifier-fail-3' }), 'revision-budget-exhausted', 'third revision round is blocked');
109
-
110
- const replanCtx = budgetContext('replan', 'fable-staged');
111
- recordBudgetEvent(replanCtx, 'replan', { evidence: 'replan-1' });
112
- recordBudgetEvent(replanCtx, 'replan', { evidence: 'replan-2' });
113
- expectBudgetBlock(() => recordBudgetEvent(replanCtx, 'replan', { evidence: 'replan-3' }), 'replan-budget-exhausted', 'third full replan is blocked');
114
-
115
- const workerCtx = budgetContext('workers', 'fable-parallel');
116
- for (const id of ['w1', 'w2', 'w3', 'w4']) recordBudgetEvent(workerCtx, 'worker-acquire', { workerId: id });
117
- check(Object.keys(workerCtx.workflow.budget.activeWorkers).length === 4, 'worker accounting reaches but does not exceed maxWorkers');
118
- recordBudgetEvent(workerCtx, 'worker-release', { workerId: 'w1' });
119
- recordBudgetEvent(workerCtx, 'worker-acquire', { workerId: 'w5' });
120
- check(Object.keys(workerCtx.workflow.budget.activeWorkers).length === 4 && workerCtx.workflow.budget.activeWorkers.w5, 'released worker capacity is reusable');
121
-
122
- const overflowCtx = budgetContext('worker-overflow', 'fable-parallel');
123
- for (const id of ['w1', 'w2', 'w3', 'w4']) recordBudgetEvent(overflowCtx, 'worker-acquire', { workerId: id });
124
- expectBudgetBlock(() => recordBudgetEvent(overflowCtx, 'worker-acquire', { workerId: 'w5' }), 'worker-budget-exhausted', 'fifth concurrent worker is blocked');
125
-
126
- try { fs.rmSync(budgetRoot, { recursive: true, force: true }); } catch (_) {}
127
-
128
- const pluginHooks = JSON.parse(fs.readFileSync(path.join(ROOT, 'plugins/harness-everything/hooks/hooks.json'), 'utf8'));
129
- check((pluginHooks.hooks.PreToolUse || []).some(entry => (entry.hooks || []).some(hook => /workflow-gate\.js/.test(hook.command || ''))), 'OpenAI plugin hook package carries workflow gate adapter');
130
- check((pluginHooks.hooks.Stop || []).some(entry => (entry.hooks || []).some(hook => /workflow-stop-gate\.js/.test(hook.command || ''))), 'OpenAI plugin hook package carries workflow completion adapter');
131
-
132
- console.log(`\n${failed === 0 ? 'PASS' : 'FAIL'}: workflow execution enforcement (${failed} failure${failed === 1 ? '' : 's'})`);
133
- process.exit(failed === 0 ? 0 : 1);
47
+ const workflow={lastMutationAt:0,verification:{independent:true}};
48
+ const fresh=runtime.unresolvedStages({runRoot:freshnessRoot,run},workflow,workerAt);
49
+ check(!fresh.includes('independent-verifier-missing'),'Fable independent verifier accepts edits older than verifier evidence');
50
+ const stale=runtime.unresolvedStages({runRoot:freshnessRoot,run},workflow,verifierAt+1000);
51
+ check(stale.includes('independent-verifier-missing'),'Fable independent verifier is invalidated by newer handoff shell-edit evidence');
52
+ }finally{fs.rmSync(freshnessRoot,{recursive:true,force:true});}
53
+
54
+ console.log('\n'+(failed===0?'PASS':'FAIL')+': workflow guidance contract ('+failed+' failures)');
55
+ process.exit(failed===0?0:1);
@@ -63,7 +63,7 @@ const result = spawnSync(process.execPath, [hook], {
63
63
  encoding: 'utf8',
64
64
  env: { ...process.env, HARNESS_STATE_HOME: stateHome },
65
65
  });
66
- check(result.status === 2, 'a check from another session cannot resolve this run contract');
66
+ check(result.status === 0 && /session|correlat|resolve/i.test(result.stderr), 'a cross-session check is reported without resolving the run contract');
67
67
  const manifest = JSON.parse(fs.readFileSync(path.join(isolated.runRoot, 'contracts', 'verify.json'), 'utf8'));
68
68
  check(manifest.status === 'planned', 'cross-session check leaves the target contract unmodified');
69
69
 
@@ -92,7 +92,7 @@ const runA = prepareRun({
92
92
  sessionId: 'phase3-session',
93
93
  });
94
94
  check(JSON.stringify(runA.execution.batches) === JSON.stringify([['architecture', 'security'], ['synthesis']]), 'parallel plan emits dependency-safe ready batches');
95
- check(runA.execution.limits.maxWorkers === 4, 'run manifest retains the numeric worker cap');
95
+ check(runA.execution.limits.maxWorkers === 4, 'run manifest retains maxWorkers as an advisory planning hint');
96
96
  check(fs.existsSync(path.join(runA.runRoot, 'contracts', 'security.json')), 'run-scoped stage contract is written');
97
97
  check(fs.existsSync(path.join(runA.runRoot, 'contracts', 'synthesis.json')), 'downstream stage has its own contract');
98
98
 
@@ -119,9 +119,9 @@ const wideRun = prepareRun({
119
119
  runId: 'phase3-wide-parallel',
120
120
  sessionId: 'wide-session',
121
121
  });
122
- check(wideRun.execution.batches.length === 2, 'ready set larger than worker cap is deterministically chunked');
123
- check(wideRun.execution.batches.every(batch => batch.length <= 4), 'no dispatchable batch exceeds maxWorkers=4');
124
- check(wideRun.execution.batches.flat().length === 6, 'worker-cap chunking preserves every ready stage');
122
+ check(wideRun.execution.batches.length === 1, 'ready set is not hard-chunked by the advisory maxWorkers hint');
123
+ check(wideRun.execution.batches[0].length === 6, 'all dependency-ready stages remain dispatchable');
124
+ check(wideRun.execution.batches.flat().length === 6, 'advisory worker guidance preserves every ready stage');
125
125
 
126
126
  const stagedRun = prepareRun({
127
127
  routerContract: stagedContract,
@@ -235,7 +235,7 @@ const ambiguousResult = spawnSync(process.execPath, [contractHook], {
235
235
  encoding: 'utf8',
236
236
  env: { ...process.env, HARNESS_STATE_HOME: stateHome },
237
237
  });
238
- check(ambiguousResult.status === 2, 'ambiguous same-command correlation blocks acceptance');
238
+ check(ambiguousResult.status === 0 && /ambiguous|multiple/i.test(ambiguousResult.stderr), 'ambiguous same-command correlation is reported without blocking');
239
239
  for (const runId of ['ambiguous-a', 'ambiguous-b']) {
240
240
  const manifest = JSON.parse(fs.readFileSync(path.join(getWorkspaceStateRoot(workspace), 'fable-runs', runId, 'contracts', 'same-check.json'), 'utf8'));
241
241
  check(manifest.status === 'planned', `${runId} remains unmodified after ambiguous check`);
@@ -280,7 +280,7 @@ check(inScope.status === 0 && /in-scope/.test(inScope.stdout), 'declared in-scop
280
280
  check(runScope('PreToolUse').status === 0, 'next burst starts from rolled-forward baseline');
281
281
  fs.writeFileSync(path.join(gitWorkspace, 'docs', 'oops.txt'), 'unexpected\n');
282
282
  const outOfScope = runScope('PostToolUse');
283
- check(outOfScope.status === 2 && /OUT-OF-SCOPE/.test(outOfScope.stderr), 'undeclared worker edit is rejected by scope guard');
283
+ check(outOfScope.status === 0 && /OUT-OF-SCOPE/.test(outOfScope.stderr), 'undeclared worker edit is reported without blocking');
284
284
 
285
285
  const contractDoc = fs.readFileSync(path.join(ROOT, 'fable-mode', 'CONTRACT-FORMAT.md'), 'utf8');
286
286
  const orchestratorDoc = fs.readFileSync(path.join(ROOT, 'fable-mode', 'agents', 'fable-orchestrator.md'), 'utf8');