harness-everything 0.16.0 → 0.16.2

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 (111) 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 +14 -0
  5. package/README.md +7 -7
  6. package/VERIFICATION.md +3 -3
  7. package/behavioral-evals/PAIRED-BENCHMARK.md +3 -3
  8. package/behavioral-evals/README.md +38 -32
  9. package/behavioral-evals/opencode-hardlock-live.js +6 -290
  10. package/behavioral-evals/opencode-reflection-gate-live.js +342 -0
  11. package/behavioral-evals/paired-benchmark.js +3 -3
  12. package/ci/consistency-check.js +20 -0
  13. package/ci/invariant-routing.test.js +58 -158
  14. package/ci/mechanism-2a-rule-of-3.test.js +73 -138
  15. package/ci/mechanism-2ac-atomic-commit-check.test.js +2 -2
  16. package/ci/mechanism-2af-worktree-isolation.test.js +38 -196
  17. package/ci/mechanism-2ah-workflow-lifecycle.test.js +20 -831
  18. package/ci/mechanism-2ai-workspace-fingerprint-batch.test.js +3 -4
  19. package/ci/mechanism-2b-boundary-guard.test.js +6 -16
  20. package/ci/mechanism-2e-scope-guard.test.js +8 -24
  21. package/ci/mechanism-2f-stop-gate.test.js +21 -75
  22. package/ci/mechanism-2n-opencode-plugin.test.js +13 -28
  23. package/ci/mechanism-31-opencode-hardlock-evidence.test.js +43 -99
  24. package/ci/openai-plugin-package.test.js +8 -8
  25. package/ci/openai-plugin-runtime.test.js +2 -2
  26. package/ci/router-workflow-plan.test.js +25 -6
  27. package/ci/self-evolve-loop.test.js +4 -1
  28. package/ci/workflow-execution-contract.test.js +51 -129
  29. package/ci/workflow-plan-consumers-edge.test.js +1 -1
  30. package/ci/workflow-plan-consumers.test.js +6 -6
  31. package/docs/architecture.md +21 -21
  32. package/docs/enforcement-locks.md +41 -102
  33. package/docs/mechanism-first-skill-mesh.md +2 -2
  34. package/docs/philosophy.md +2 -2
  35. package/docs/platform-capabilities.md +3 -3
  36. package/docs/routing.md +14 -14
  37. package/docs/workflow-runtime.md +80 -110
  38. package/evals/harness-everything/tasks/positive-tier1.yaml +1 -1
  39. package/evals/harness-everything/tasks/positive-tier2.yaml +1 -1
  40. package/evals/harness-everything/tasks/positive-tier3.yaml +1 -1
  41. package/evals/using-git-worktrees/tasks/positive-trigger.yaml +1 -1
  42. package/fable-mode/SKILL.md +1 -1
  43. package/fable-mode/agents/fable-orchestrator.md +7 -6
  44. package/fable-mode/execution-guardrails/SKILL.md +1 -1
  45. package/fable-mode/fable-haiku/SKILL.md +1 -1
  46. package/fable-mode/fable-opus/SKILL.md +1 -1
  47. package/fable-mode/fable-sonnet/SKILL.md +1 -1
  48. package/fable-mode/scripts/workflow-plan-consumer.js +4 -3
  49. package/harness-everything/SKILL.md +11 -11
  50. package/harness-everything/references/router-workflow-plan.md +2 -2
  51. package/harness-everything/references/triage-and-tiers.md +11 -11
  52. package/harness-everything/scripts/kernel-router-core.js +5 -5
  53. package/harness-everything/scripts/kernel-router.js +16 -21
  54. package/harness-everything/scripts/router-contract-core.js +1 -1
  55. package/hooks/hooks.json +10 -23
  56. package/hooks/scripts/atomic-commit-check.js +1 -1
  57. package/hooks/scripts/boundary-guard.js +4 -4
  58. package/hooks/scripts/contract-test.js +8 -11
  59. package/hooks/scripts/depth-guard.js +2 -2
  60. package/hooks/scripts/lib/workflow-runtime.js +24 -402
  61. package/hooks/scripts/rule-of-3-tracker.js +28 -31
  62. package/hooks/scripts/rule-of-3.js +4 -28
  63. package/hooks/scripts/state-persist.js +36 -136
  64. package/hooks/scripts/stop-gate.js +15 -48
  65. package/hooks/scripts/subagent-scope-guard.js +8 -8
  66. package/hooks/scripts/workflow-disposition.js +2 -10
  67. package/hooks/scripts/workflow-gate.js +33 -84
  68. package/hooks/scripts/workflow-mutation-denied.js +3 -15
  69. package/hooks/scripts/workflow-stop-gate.js +23 -38
  70. package/opencode-plugin/README.md +10 -13
  71. package/opencode-plugin/index.mjs +14 -30
  72. package/opencode-plugin/plugin.json +1 -1
  73. package/package.json +3 -2
  74. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  75. package/plugins/harness-everything/hooks/hooks.json +3 -3
  76. package/plugins/harness-everything/hooks/scripts/atomic-commit-check.js +1 -1
  77. package/plugins/harness-everything/hooks/scripts/boundary-guard.js +4 -4
  78. package/plugins/harness-everything/hooks/scripts/contract-test.js +8 -11
  79. package/plugins/harness-everything/hooks/scripts/depth-guard.js +2 -2
  80. package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +24 -402
  81. package/plugins/harness-everything/hooks/scripts/rule-of-3-tracker.js +28 -31
  82. package/plugins/harness-everything/hooks/scripts/rule-of-3.js +4 -28
  83. package/plugins/harness-everything/hooks/scripts/state-persist.js +36 -136
  84. package/plugins/harness-everything/hooks/scripts/stop-gate.js +15 -48
  85. package/plugins/harness-everything/hooks/scripts/subagent-scope-guard.js +8 -8
  86. package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +2 -10
  87. package/plugins/harness-everything/hooks/scripts/workflow-gate.js +33 -84
  88. package/plugins/harness-everything/hooks/scripts/workflow-mutation-denied.js +3 -15
  89. package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +23 -38
  90. package/plugins/harness-everything/plugin.json +1 -1
  91. package/plugins/harness-everything/scripts/lib/advisory-text.js +16 -29
  92. package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
  93. package/plugins/harness-everything/skills/fable-mode/agents/fable-orchestrator.md +7 -6
  94. package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
  95. package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
  96. package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
  97. package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
  98. package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +4 -3
  99. package/plugins/harness-everything/skills/harness-everything/SKILL.md +11 -11
  100. package/plugins/harness-everything/skills/harness-everything/references/router-workflow-plan.md +2 -2
  101. package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +11 -11
  102. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +5 -5
  103. package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +16 -21
  104. package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +1 -1
  105. package/plugins/harness-everything/skills/using-git-worktrees/SKILL.md +5 -5
  106. package/plugins/harness-everything/skills/zoom-out/SKILL.md +3 -3
  107. package/plugins/harness-everything/skills/zoom-out/references/circuit-breaker.md +4 -4
  108. package/scripts/lib/advisory-text.js +16 -29
  109. package/using-git-worktrees/SKILL.md +5 -5
  110. package/zoom-out/SKILL.md +3 -3
  111. package/zoom-out/references/circuit-breaker.md +4 -4
@@ -4,178 +4,113 @@ const helper = require('./test-helper');
4
4
 
5
5
  console.log('\n[2a] Rule of 3 circuit breaker...');
6
6
 
7
- const hooksConfig = JSON.parse(
8
- fs.readFileSync(path.join(helper.root, 'hooks', 'hooks.json'), 'utf8')
9
- );
10
- const failureTrackerHook = (hooksConfig.hooks.PostToolUseFailure || [])
11
- .find(entry => entry.id === 'harness:post-failure:rule-of-3-tracker');
7
+ const hooksConfig = JSON.parse(fs.readFileSync(path.join(helper.root, 'hooks', 'hooks.json'), 'utf8'));
8
+ const preRuleHook = (hooksConfig.hooks.PreToolUse || []).find(entry => entry.id === 'harness:pre:rule-of-3');
9
+ const successTrackerHook = (hooksConfig.hooks.PostToolUse || []).find(entry => entry.id === 'harness:post:rule-of-3-tracker');
10
+ const failureTrackerHook = (hooksConfig.hooks.PostToolUseFailure || []).find(entry => entry.id === 'harness:post-failure:rule-of-3-tracker');
11
+ const matcherHas = (entry, tool) => Boolean(entry && String(entry.matcher || '').split('|').includes(tool));
12
12
  helper.check(
13
- '2a-claude-wiring. PostToolUseFailure invokes the Rule-of-3 tracker',
14
- failureTrackerHook &&
15
- failureTrackerHook.matcher === 'Bash|PowerShell' &&
13
+ '2a-wiring. PostToolUseFailure invokes the Rule-of-3 tracker',
14
+ matcherHas(failureTrackerHook, 'Bash') && matcherHas(failureTrackerHook, 'PowerShell') &&
15
+ matcherHas(failureTrackerHook, 'apply_patch') &&
16
16
  failureTrackerHook.hooks?.some(hook => hook.command.includes('rule-of-3-tracker.js')),
17
- `Hook=${JSON.stringify(failureTrackerHook)}`
17
+ JSON.stringify(failureTrackerHook)
18
+ );
19
+ helper.check(
20
+ '2a-wiring. apply_patch is covered before and after execution',
21
+ matcherHas(preRuleHook, 'apply_patch') && matcherHas(successTrackerHook, 'apply_patch') && matcherHas(failureTrackerHook, 'apply_patch'),
22
+ JSON.stringify({ preRuleHook, successTrackerHook, failureTrackerHook })
18
23
  );
19
24
 
20
- const claudeFailurePayload = {
25
+ const failure = {
21
26
  hook_event_name: 'PostToolUseFailure',
22
27
  tool_name: 'Bash',
23
28
  tool_input: { command: 'node -e "process.exit(1)"' },
24
29
  error: 'Exit code 1\ncommand failed',
25
30
  session_id: helper.SESSION_ID,
26
31
  };
27
- const failureRuns = [1, 2, 3].map(() =>
28
- helper.runHook('rule-of-3-tracker.js', claudeFailurePayload)
29
- );
30
- const trackedFailureState = helper.readState('rule-of-3-state.json');
32
+ for (let i = 0; i < 3; i++) helper.runHook('rule-of-3-tracker.js', failure);
33
+ let state = helper.readState('rule-of-3-state.json');
31
34
  helper.check(
32
- '2a-claude-failure. PostToolUseFailure error payload increments the same signature to threshold',
33
- failureRuns.every(result => result.code === 0) &&
34
- trackedFailureState.count === 3 &&
35
- trackedFailureState.threshold === 3 &&
36
- trackedFailureState.lastHash &&
37
- trackedFailureState.zoomOutResolved === false,
38
- `Runs=${JSON.stringify(failureRuns)}, state=${JSON.stringify(trackedFailureState)}`
35
+ '2a-three. exactly three matching failures arm zoom-out',
36
+ state.count === 3 && state.lastHash && state.zoomOutResolved === false && state.threshold === undefined,
37
+ JSON.stringify(state)
39
38
  );
40
39
 
41
- const claudeTripResult = helper.runHook('rule-of-3.js', {
42
- hook_event_name: 'PreToolUse',
43
- tool_name: 'Bash',
44
- tool_input: { command: 'echo retry' },
45
- session_id: helper.SESSION_ID,
46
- });
40
+ const trip = helper.runHook('rule-of-3.js', { hook_event_name: 'PreToolUse', tool_name: 'Bash', session_id: helper.SESSION_ID });
47
41
  helper.check(
48
- '2a-claude-trip. Tracked Claude failures block the next mutation',
49
- claudeTripResult.code === 2 &&
50
- claudeTripResult.stderr.includes('RULE OF 3 CIRCUIT BREAKER TRIGGERED'),
51
- `Got exit=${claudeTripResult.code}, stderr="${claudeTripResult.stderr.slice(0, 200)}"`
42
+ '2a-trip. third matching failure blocks mutation for reflection',
43
+ trip.code === 2 && trip.stderr.includes('RULE OF 3 CIRCUIT BREAKER TRIGGERED'),
44
+ trip.stderr
52
45
  );
53
46
 
54
- const successResult = helper.runHook('rule-of-3-tracker.js', {
55
- hook_event_name: 'PostToolUse',
56
- tool_name: 'Bash',
57
- tool_input: { command: 'echo success' },
58
- tool_response: { stdout: 'success', stderr: '', exitCode: 0 },
59
- session_id: helper.SESSION_ID,
60
- });
61
- const resetState = helper.readState('rule-of-3-state.json');
47
+ helper.writeState('rule-of-3-state.json', { count: 3, lastHash: 'mech-test', zoomOutResolved: false, lastFailureAt: 0, zoomOutCycles: 0 });
48
+ fs.writeFileSync(path.join(helper.sessionDir, 'zoom-out-report.md'),
49
+ '## Goal\nx\n## Failed Attempts\nx\n## Verified Facts\nx\n## Diagnosis\nx\n## Decision\nRESUME: new approach\n', 'utf8');
50
+ const release = helper.runHook('rule-of-3.js', { session_id: helper.SESSION_ID });
51
+ state = helper.readState('rule-of-3-state.json');
62
52
  helper.check(
63
- '2a-claude-success. Confirmed PostToolUse success keeps the existing reset behavior',
64
- successResult.code === 0 &&
65
- resetState.count === 0 &&
66
- resetState.zoomOutResolved === true &&
67
- resetState.zoomOutCycles === 0,
68
- `Got exit=${successResult.code}, state=${JSON.stringify(resetState)}`
53
+ '2a-release. valid reflection releases breaker and resets count',
54
+ release.code === 0 && state.count === 0 && state.zoomOutResolved === true && state.zoomOutCycles === 1,
55
+ JSON.stringify(state)
69
56
  );
70
57
 
71
- helper.runHook('rule-of-3-tracker.js', claudeFailurePayload);
72
- const claudeNoExitSuccess = helper.runHook('rule-of-3-tracker.js', {
73
- hook_event_name: 'PostToolUse',
74
- tool_name: 'Bash',
75
- tool_input: { command: 'echo recovered' },
76
- tool_response: {
77
- stdout: 'recovered',
78
- stderr: 'warning: successful command emitted an error-like diagnostic',
79
- },
80
- session_id: helper.SESSION_ID,
81
- });
82
- const noExitResetState = helper.readState('rule-of-3-state.json');
58
+ helper.writeState('rule-of-3-state.json', { count: 3, lastHash: 'mech-test', zoomOutResolved: false, lastFailureAt: Date.now() + 60000, zoomOutCycles: 1 });
59
+ const retrip = helper.runHook('rule-of-3.js', { session_id: helper.SESSION_ID });
83
60
  helper.check(
84
- '2a-claude-success-no-exit. PostToolUse without exitCode resets even with stderr diagnostics',
85
- claudeNoExitSuccess.code === 0 &&
86
- noExitResetState.count === 0 &&
87
- noExitResetState.zoomOutResolved === true,
88
- `Got exit=${claudeNoExitSuccess.code}, state=${JSON.stringify(noExitResetState)}`
61
+ '2a-repeat. later three-failure cycle requests another zoom-out, not permanent hard lock',
62
+ retrip.code === 2 && retrip.stderr.includes('RULE OF 3 CIRCUIT BREAKER TRIGGERED') && !/hard lock|repeat trip/i.test(retrip.stderr),
63
+ retrip.stderr
89
64
  );
90
65
 
91
- helper.writeState('rule-of-3-state.json', {
92
- count: 0, lastHash: null, zoomOutResolved: false, zoomOutCycles: 0, lastFailureAt: 0,
66
+ helper.writeState('rule-of-3-state.json', { count: 0, lastHash: null, zoomOutResolved: false, zoomOutCycles: 0, lastFailureAt: 0 });
67
+ for (let i = 0; i < 2; i++) helper.runHook('rule-of-3-tracker.js', {
68
+ hook_event_name: 'PostToolUseFailure', tool_name: 'Bash',
69
+ tool_input: { command: 'cat protected.txt' }, error: 'EACCES: permission denied, open protected.txt',
70
+ session_id: helper.SESSION_ID,
93
71
  });
72
+ state = helper.readState('rule-of-3-state.json');
73
+ helper.check('2a-category. permission failures still use the same threshold of three', state.count === 2 && state.category === 'permission' && state.threshold === undefined, JSON.stringify(state));
74
+ const beforeThird = helper.runHook('rule-of-3.js', { session_id: helper.SESSION_ID });
75
+ helper.check('2a-category-two. two permission failures do not trip early', beforeThird.code === 0, beforeThird.stderr);
94
76
  helper.runHook('rule-of-3-tracker.js', {
95
- hook_event_name: 'PostToolUseFailure',
96
- tool_name: 'Bash',
97
- tool_input: { command: 'diff -rq hooks cache-hooks' },
98
- error: 'Exit code 1\nworkflow-mutation-denied.js\nFiles hooks/a.js and cache-hooks/a.js differ',
77
+ hook_event_name: 'PostToolUseFailure', tool_name: 'Bash',
78
+ tool_input: { command: 'cat protected.txt' }, error: 'EACCES: permission denied, open protected.txt',
99
79
  session_id: helper.SESSION_ID,
100
80
  });
101
- const deniedFilenameState = helper.readState('rule-of-3-state.json');
102
- helper.check(
103
- '2a-category-negative. File name containing denied stays unknown/threshold 3',
104
- deniedFilenameState.category === 'unknown' && deniedFilenameState.threshold === 3,
105
- `State=${JSON.stringify(deniedFilenameState)}`
106
- );
81
+ const third = helper.runHook('rule-of-3.js', { session_id: helper.SESSION_ID });
82
+ helper.check('2a-category-three. third permission failure trips normally', third.code === 2, third.stderr);
107
83
 
108
- helper.writeState('rule-of-3-state.json', {
109
- count: 0, lastHash: null, zoomOutResolved: false, zoomOutCycles: 0, lastFailureAt: 0,
110
- });
111
- helper.runHook('rule-of-3-tracker.js', {
84
+ helper.writeState('rule-of-3-state.json', { count: 0, lastHash: null, lastOperationHash: null, zoomOutResolved: false, zoomOutCycles: 0, lastFailureAt: 0 });
85
+ const repeatedFailure = {
112
86
  hook_event_name: 'PostToolUseFailure',
113
87
  tool_name: 'Bash',
114
- tool_input: { command: 'cat protected.txt' },
115
- error: 'EACCES: permission denied, open protected.txt',
88
+ tool_input: { command: 'node flaky.js' },
89
+ error: 'Error: flaky failure',
116
90
  session_id: helper.SESSION_ID,
117
- });
118
- const permissionState = helper.readState('rule-of-3-state.json');
119
- helper.check(
120
- '2a-category-permission. Real EACCES is permission/threshold 2',
121
- permissionState.category === 'permission' && permissionState.threshold === 2,
122
- `State=${JSON.stringify(permissionState)}`
123
- );
124
-
125
- helper.writeState('rule-of-3-state.json', {
126
- count: 0, lastHash: null, zoomOutResolved: false, zoomOutCycles: 0, lastFailureAt: 0,
127
- });
91
+ };
92
+ helper.runHook('rule-of-3-tracker.js', repeatedFailure);
93
+ helper.runHook('rule-of-3-tracker.js', repeatedFailure);
128
94
  helper.runHook('rule-of-3-tracker.js', {
129
- hook_event_name: 'PostToolUseFailure',
95
+ hook_event_name: 'PostToolUse',
130
96
  tool_name: 'Bash',
131
- tool_input: { command: 'node slow.js' },
132
- error: 'TimeoutError: operation timed out',
97
+ tool_input: { command: 'git status --short' },
98
+ tool_response: { exitCode: 0, stdout: '' },
133
99
  session_id: helper.SESSION_ID,
134
100
  });
135
- const timeoutState = helper.readState('rule-of-3-state.json');
136
- helper.check(
137
- '2a-category-timeout. Real timeout is timeout/threshold 2',
138
- timeoutState.category === 'timeout' && timeoutState.threshold === 2,
139
- `State=${JSON.stringify(timeoutState)}`
140
- );
141
-
142
- helper.writeState('rule-of-3-state.json', { count: 3, lastHash: 'mech-test', zoomOutResolved: false });
143
- const tripResult = helper.runHook('rule-of-3.js', { session_id: helper.SESSION_ID });
144
- helper.check(
145
- '2a. Trips at count=3 (exit=2, CRITICAL banner)',
146
- tripResult.code === 2 && tripResult.stderr.includes('RULE OF 3 CIRCUIT BREAKER TRIGGERED'),
147
- `Got exit=${tripResult.code}, stderr="${tripResult.stderr.slice(0, 200)}"`
148
- );
149
-
150
- helper.writeState('rule-of-3-state.json', {
151
- count: 3, lastHash: 'mech-test', zoomOutResolved: false, lastFailureAt: 0, zoomOutCycles: 0,
152
- });
153
- fs.writeFileSync(
154
- path.join(helper.sessionDir, 'zoom-out-report.md'),
155
- '## Goal\nx\n## Failed Attempts\nx\n## Verified Facts\nx\n## Diagnosis\nx\n## Decision\nRESUME: new approach\n',
156
- 'utf8'
157
- );
158
- const releaseResult = helper.runHook('rule-of-3.js', { session_id: helper.SESSION_ID });
159
- const releasedState = helper.readState('rule-of-3-state.json');
160
- helper.check(
161
- '2a-bis. Valid zoom-out report releases the breaker (exit=0, count reset)',
162
- releaseResult.code === 0 &&
163
- releaseResult.stdout.includes('breaker released') &&
164
- releasedState.count === 0 &&
165
- releasedState.zoomOutResolved === true &&
166
- releasedState.zoomOutCycles === 1,
167
- `Got exit=${releaseResult.code}, state=${JSON.stringify(releasedState)}`
168
- );
169
-
170
- helper.writeState('rule-of-3-state.json', {
171
- count: 3, lastHash: 'mech-test', zoomOutResolved: false,
172
- lastFailureAt: Date.now() + 60000, zoomOutCycles: 1,
101
+ state = helper.readState('rule-of-3-state.json');
102
+ helper.check('2a-interleaved. unrelated success preserves the matching failure count', state.count === 2 && state.lastHash, JSON.stringify(state));
103
+ helper.runHook('rule-of-3-tracker.js', repeatedFailure);
104
+ state = helper.readState('rule-of-3-state.json');
105
+ helper.check('2a-interleaved-three. third matching failure still arms zoom-out after unrelated success', state.count === 3 && state.zoomOutResolved === false, JSON.stringify(state));
106
+ helper.runHook('rule-of-3-tracker.js', {
107
+ hook_event_name: 'PostToolUse',
108
+ tool_name: 'Bash',
109
+ tool_input: { command: 'node flaky.js' },
110
+ tool_response: { exitCode: 0, stdout: 'recovered' },
111
+ session_id: helper.SESSION_ID,
173
112
  });
174
- const hardLockResult = helper.runHook('rule-of-3.js', { session_id: helper.SESSION_ID });
175
- helper.check(
176
- '2a-ter. Second trip on same signature hard-locks (exit=2, repeat trip)',
177
- hardLockResult.code === 2 && hardLockResult.stderr.includes('repeat trip - hard lock'),
178
- `Got exit=${hardLockResult.code}, stderr="${hardLockResult.stderr.slice(0, 200)}"`
179
- );
113
+ state = helper.readState('rule-of-3-state.json');
114
+ helper.check('2a-resolved. same-operation success clears the resolved signature', state.count === 0 && state.lastHash === null && state.lastOperationHash === null, JSON.stringify(state));
180
115
 
181
116
  helper.finish();
@@ -87,7 +87,7 @@ try {
87
87
  fs.writeFileSync(path.join(repo, 'tracked.txt'), 'changed\n', 'utf8');
88
88
  const dirtyResults = sixInvocations({ session: 'dirty-tree', target: path.join(repo, 'tracked.txt') });
89
89
  assert.ok(dirtyResults.slice(0, 5).every(result => result.status === 0), 'first five in-workspace edits remain non-blocking');
90
- assert.strictEqual(dirtyResults[5].status, 2, 'sixth in-workspace edit with uncommitted changes nudges');
90
+ assert.strictEqual(dirtyResults[5].status, 0, 'sixth in-workspace edit with uncommitted changes remains non-blocking');
91
91
  assert.match(String(dirtyResults[5].stderr || ''), /6 edits since the last commit/, 'nudge wording is tool-neutral');
92
92
  assert.doesNotMatch(String(dirtyResults[5].stderr || ''), /apply_patch calls/, 'nudge no longer claims all edits are apply_patch calls');
93
93
 
@@ -97,7 +97,7 @@ try {
97
97
  tool: 'apply_patch',
98
98
  patch: '*** Begin Patch\n*** Update File: tracked.txt\n@@\n-old\n+new\n*** End Patch',
99
99
  });
100
- assert.strictEqual(patchResults[5].status, 2, 'in-workspace apply_patch header participates in the threshold');
100
+ assert.strictEqual(patchResults[5].status, 0, 'in-workspace apply_patch reminder threshold remains non-blocking');
101
101
 
102
102
  // Moving HEAD resets the counter for the same session.
103
103
  git(['add', 'tracked.txt']);
@@ -1,198 +1,40 @@
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
-
9
- const ROOT = path.resolve(__dirname, '..');
10
- const GATE = path.join(ROOT, 'hooks', 'scripts', 'workflow-gate.js');
11
- let failed = 0;
12
-
13
- function check(condition, message) {
14
- if (condition) console.log(`✅ worktree-isolation: ${message}`);
15
- else { console.error(`❌ worktree-isolation: ${message}`); failed++; }
16
- }
17
-
18
- function git(cwd, args) {
19
- return spawnSync('git', args, { cwd, encoding: 'utf8', windowsHide: true });
20
- }
21
-
22
- const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-worktree-isolation-'));
23
- const home = path.join(temp, 'home');
24
- const repo = path.join(temp, 'repo');
25
- const linked = path.join(temp, 'repo-linked');
26
- fs.mkdirSync(home, { recursive: true });
27
- fs.mkdirSync(repo, { recursive: true });
28
-
29
- const env = {
30
- ...process.env,
31
- HOME: home,
32
- USERPROFILE: home,
33
- HARNESS_STATE_HOME: path.join(home, '.agents', 'harness-everything'),
34
- CLAUDE: '1',
35
- };
36
- process.env.HARNESS_STATE_HOME = env.HARNESS_STATE_HOME;
37
- process.env.HOME = home;
38
- process.env.USERPROFILE = home;
39
-
40
- try {
41
- if (process.platform === 'win32') {
42
- const { key } = require('../hooks/scripts/lib/workflow-isolation');
43
- const shortProgramFiles = path.join(process.env.SystemDrive || 'C:', 'PROGRA~1');
44
- if (fs.existsSync(shortProgramFiles)) {
45
- check(key(shortProgramFiles) === key(fs.realpathSync.native(shortProgramFiles)), 'Windows short and long path spellings identify the same physical directory');
46
- }
47
- }
48
- check(git(repo, ['init']).status === 0, 'fixture git repository initializes');
49
- fs.writeFileSync(path.join(repo, 'README.md'), 'fixture\n', 'utf8');
50
- check(git(repo, ['add', 'README.md']).status === 0, 'fixture file stages');
51
- check(git(repo, ['-c', 'user.name=Harness Test', '-c', 'user.email=harness@example.invalid', 'commit', '-m', 'fixture']).status === 0, 'fixture baseline commits');
52
-
53
- const workspace = require(path.join(ROOT, 'scripts', 'lib', 'workspace'));
54
- const state = require(path.join(ROOT, 'hooks', 'scripts', 'lib', 'harness-state'));
55
- const sessionId = 'worktree-isolation-test';
56
- const context = { session_id: sessionId, host_id: 'mechanism-test', cwd: repo };
57
- workspace.bindWorkspaceSession(sessionId, repo, context, true);
58
- const sessionDir = state.getSessionDir(repo, sessionId, context);
59
- const workflowFile = path.join(sessionDir, 'workflow-run.json');
60
-
61
- function writeWorkflow(tier, strategy) {
62
- fs.writeFileSync(workflowFile, `${JSON.stringify({
63
- schemaVersion: 1,
64
- createdAt: new Date().toISOString(),
65
- createdAtMs: Date.now(),
66
- sessionId,
67
- tier,
68
- strategy,
69
- state: 'active',
70
- }, null, 2)}\n`, 'utf8');
71
- }
72
-
73
- function runGate(tool, cwd, input = {}) {
74
- const payload = {
75
- session_id: sessionId,
76
- host_id: 'mechanism-test',
77
- cwd,
78
- tool_name: tool,
79
- tool_input: input,
80
- };
81
- return spawnSync(process.execPath, [GATE], {
82
- cwd: ROOT,
83
- env,
84
- input: JSON.stringify(payload),
85
- encoding: 'utf8',
86
- });
87
- }
88
-
89
- // Tier 3 isolation is independent of the selected execution topology.
90
- writeWorkflow('tier3', 'iterative-single');
91
-
92
- const primaryWrite = runGate('Write', repo, { file_path: path.join(repo, 'src.js'), content: 'x' });
93
- check(primaryWrite.status === 2, 'Tier 3 direct write is blocked in the primary working tree');
94
- check(/worktree isolation/i.test(primaryWrite.stderr), 'blocked write explains the isolation requirement');
95
-
96
- const primaryDelete = runGate('Bash', repo, { command: 'rm -rf src' });
97
- check(primaryDelete.status === 2, 'Tier 3 destructive shell mutation is blocked in the primary working tree');
98
-
99
- for (const command of ['git status > tracked.txt', 'git log --output=tracked.txt', 'find . -delete', 'fd -x rm', 'rg --pre touch x', 'git show $(touch tracked.txt)', 'Get-Content (Remove-Item src)']) {
100
- check(runGate('Bash', repo, { command }).status === 2, `shell side effect is not read-only: ${command}`);
101
- }
102
-
103
- const chainedBypass = runGate('Bash', repo, { command: 'git status --short && rm -rf src' });
104
- check(chainedBypass.status === 2, 'read-only prefix cannot hide a chained mutation');
105
-
106
- const readOnly = runGate('Bash', repo, { command: 'git status --short' });
107
- check(readOnly.status === 0, 'read-only discovery remains allowed before isolation');
108
-
109
- // #82-family follow-up: chaining/redirects that are hidden behind the old
110
- // blanket metacharacter reject must stay blocked, but a compound command
111
- // whose every segment is independently trusted read-only must not burn
112
- // iteration budget or the mutation-timestamp just because it uses `;`,
113
- // `&&`, `|`, or a non-writing `2>&1` redirect.
114
- for (const command of [
115
- 'git status --short; git log --oneline -1',
116
- 'git status --short && git log --oneline -1',
117
- 'git log --oneline -1 | head -1',
118
- 'echo hello',
119
- 'echo hello; git status --short',
120
- 'git status --short 2>&1',
121
- 'gh issue view 1 --repo octocat/hello-world --json number,title 2>&1',
122
- 'gh pr list --repo octocat/hello-world --json number | head -1',
123
- 'gh repo view octocat/hello-world --json name',
124
- 'gh auth status',
125
- 'cat README.md | grep fixture',
126
- 'grep -n "fixture\\|missing" README.md | head -20',
127
- "grep -n 'fixture|missing' README.md | head -20",
128
- 'echo "fixture|missing" | grep fixture',
129
- 'git log --grep="fix|feat" | head -5',
130
- "grep 'fixture;missing' README.md | head -1",
131
- 'grep fixture\\|missing README.md | head -1',
132
- 'echo "2>&1"',
133
- 'echo "safe; rm -rf src"',
134
- ]) {
135
- check(runGate('Bash', repo, { command }).status === 0, `trusted read-only chain remains allowed: ${command}`);
136
- }
137
-
138
- for (const command of [
139
- 'git status --short; rm -rf src',
140
- 'git status --short | rm -rf src',
141
- 'echo hi & rm -rf src',
142
- 'git status --short > tracked.txt',
143
- 'git status --short 2>tracked.txt',
144
- 'git worktree add "harness-chain-probe" -b harness-chain-probe; rm -rf src',
145
- 'gh issue comment 1 --body hi',
146
- 'gh issue close 1',
147
- 'git status --short || rm -rf src',
148
- 'grep "unterminated README.md | head -1',
149
- "grep 'unterminated README.md | head -1",
150
- 'echo safe | rm -rf src',
151
- ]) {
152
- check(runGate('Bash', repo, { command }).status === 2, `chained/redirected command is not read-only: ${command}`);
153
- }
154
-
155
- const setup = runGate('Bash', repo, { command: `git worktree add "${linked}" -b harness-isolation-test` });
156
- check(setup.status === 0, 'git worktree creation is allowed as the isolation transition');
157
-
158
- const createLinked = git(repo, ['worktree', 'add', linked, '-b', 'harness-isolation-test']);
159
- check(createLinked.status === 0, 'fixture linked worktree is created');
160
-
161
- const isolatedWrite = runGate('Write', linked, { file_path: path.join(linked, 'src.js'), content: 'x' });
162
- check(isolatedWrite.status === 0, `Tier 3 mutation is allowed after entering a linked worktree (${isolatedWrite.stderr.trim() || 'allowed'})`);
163
-
164
- check(runGate('Write', linked, { file_path: path.join(repo, 'src.js') }).status === 2, 'linked cwd cannot authorize a primary-tree target');
165
- check(runGate('Write', linked, { file_path: '../repo/src.js' }).status === 2, 'relative traversal cannot escape the worktree');
166
- check(runGate('apply_patch', linked, `*** Begin Patch\n*** Add File: ${path.join(repo, 'src.js')}\n+x\n*** End Patch`).status === 2, 'raw patch targets are checked');
167
- check(runGate('apply_patch', linked, { patch: `*** Begin Patch\n*** Update File: README.md\n*** Move to: ${path.join(repo, 'moved.md')}\n@@\n-fixture\n+x\n*** End Patch` }).status === 2, 'patch move destination cannot escape isolation');
168
- check(runGate('exec_command', repo, { cmd: 'rm -rf src', workdir: repo }).status === 2, 'native shell alias cannot skip mutation gating');
169
- check(runGate('Bash', linked, { command: `rm "${path.join(repo, 'README.md')}"` }).status === 2, 'explicit primary-tree shell target is blocked');
170
- check(runGate('Bash', linked, { command: 'cd ../repo && node build.js' }).status === 2, 'shell directory change cannot hide the effective working directory');
171
- check(runGate('Write', linked, {}).status === 2, 'unknown direct target is blocked');
172
- const junction = path.join(linked, 'outside');
173
- fs.symlinkSync(repo, junction, process.platform === 'win32' ? 'junction' : 'dir');
174
- check(runGate('Write', linked, { file_path: path.join(junction, 'src.js') }).status === 2, 'symlink or junction escape is blocked');
175
- fs.unlinkSync(junction);
176
-
177
- const escaped = JSON.parse(fs.readFileSync(workflowFile, 'utf8'));
178
- escaped.state = 'escaped';
179
- fs.writeFileSync(workflowFile, JSON.stringify(escaped));
180
- check(runGate('Write', repo, { file_path: path.join(repo, 'src.js') }).status === 2, 'legacy escaped state never waives isolation');
181
- escaped.state = 'blocked';
182
- fs.writeFileSync(workflowFile, JSON.stringify(escaped));
183
- check(runGate('Write', linked, { file_path: path.join(linked, 'src.js') }).status === 2, 'blocked workflow cannot mutate even inside isolation');
184
- fs.writeFileSync(workflowFile, '{broken');
185
- check(runGate('Write', linked, { file_path: path.join(linked, 'src.js') }).status === 2, 'corrupt recorded workflow cannot silently authorize mutation');
186
-
187
- writeWorkflow('tier2', 'iterative-single');
188
- const tier2Write = runGate('Write', repo, { file_path: path.join(repo, 'small.js'), content: 'x' });
189
- check(tier2Write.status === 0, 'Tier 2 is not globally forced into worktree isolation');
190
- } finally {
191
- try { fs.rmSync(temp, { recursive: true, force: true }); } catch (_) { /* best effort */ }
192
- }
193
-
194
- if (failed > 0) {
195
- console.error(`FAIL: worktree isolation gate (${failed} failure${failed === 1 ? '' : 's'})`);
196
- process.exit(1);
197
- }
198
- console.log('PASS: major-workflow worktree isolation gate');
3
+ const fs=require('fs'),os=require('os'),path=require('path'),{spawnSync}=require('child_process');
4
+ const ROOT=path.resolve(__dirname,'..'),GATE=path.join(ROOT,'hooks/scripts/workflow-gate.js');
5
+ let failed=0;function check(c,m,d=''){if(c)console.log('PASS '+m);else{console.error('FAIL '+m+(d?': '+d:''));failed++;}}
6
+ const temp=fs.mkdtempSync(path.join(os.tmpdir(),'harness-worktree-guidance-')),home=path.join(temp,'home'),repo=path.join(temp,'repo'),linked=path.join(temp,'linked');
7
+ fs.mkdirSync(home,{recursive:true});fs.mkdirSync(repo,{recursive:true});
8
+ const env={...process.env,HOME:home,USERPROFILE:home,HARNESS_STATE_HOME:path.join(home,'state'),HARNESS_WORKSPACE_ROOT:repo,CLAUDE:'1'};
9
+ process.env.HARNESS_STATE_HOME=env.HARNESS_STATE_HOME;process.env.HARNESS_WORKSPACE_ROOT=repo;process.env.HOME=home;process.env.USERPROFILE=home;
10
+ function git(cwd,args){return spawnSync('git',args,{cwd,encoding:'utf8'});}
11
+ function runGate(tool,cwd,input={}){return spawnSync(process.execPath,[GATE],{cwd:ROOT,env,input:JSON.stringify({session_id:'worktree-guidance',host_id:'mechanism-test',cwd,tool_name:tool,tool_input:input}),encoding:'utf8'});}
12
+ try{
13
+ check(git(repo,['init']).status===0,'fixture repository initializes');
14
+ fs.writeFileSync(path.join(repo,'README.md'),'fixture\n');git(repo,['add','README.md']);git(repo,['-c','user.name=Harness','-c','user.email=h@example.invalid','commit','-m','init']);
15
+ const workspace=require(path.join(ROOT,'scripts/lib/workspace')); const state=require(path.join(ROOT,'hooks/scripts/lib/harness-state')); const context={session_id:'worktree-guidance',host_id:'mechanism-test',cwd:repo}; workspace.bindWorkspaceSession('worktree-guidance',repo,context,true); const dir=state.getSessionDir(repo,'worktree-guidance',context);
16
+ fs.writeFileSync(path.join(dir,'workflow-run.json'),JSON.stringify({schemaVersion:1,sessionId:'worktree-guidance',tier:'tier3',strategy:'iterative-single',state:'active'}));
17
+ const primary=runGate('Write',repo,{file_path:path.join(repo,'src.js'),content:'x'});
18
+ check(primary.status===0,'Tier-3 primary-tree mutation is not hard-blocked',primary.stderr);
19
+ check(/worktree/i.test(primary.stderr),'Tier-3 primary-tree mutation emits worktree guidance',primary.stderr);
20
+ const mutator=runGate('Bash',repo,{command:'git status --short && rm -rf src'});
21
+ check(mutator.status===0,'mutation-shaped shell command remains available',mutator.stderr);
22
+ check(/worktree/i.test(mutator.stderr),'mutation-shaped shell command receives worktree guidance',mutator.stderr);
23
+ const readOnly=runGate('Bash',repo,{command:'git status --short'});
24
+ check(readOnly.status===0,'read-only discovery remains available');
25
+ const create=git(repo,['worktree','add',linked,'-b','guidance-test']);check(create.status===0,'linked worktree fixture creates',create.stderr);
26
+ const isolated=runGate('Write',linked,{file_path:path.join(linked,'src.js'),content:'x'});
27
+ check(isolated.status===0,'linked-worktree mutation remains available',isolated.stderr);
28
+ const escape=runGate('Write',linked,{file_path:path.join(repo,'escape.js'),content:'x'});
29
+ check(escape.status===0,'cross-worktree target is reminder-only',escape.stderr);
30
+ check(/outside|worktree|target/i.test(escape.stderr),'cross-worktree target emits scope/isolation guidance',escape.stderr);
31
+ const wf=JSON.parse(fs.readFileSync(path.join(dir,'workflow-run.json'),'utf8'));wf.state='blocked';fs.writeFileSync(path.join(dir,'workflow-run.json'),JSON.stringify(wf));
32
+ const legacy=runGate('Write',linked,{file_path:path.join(linked,'legacy.js'),content:'x'});
33
+ check(legacy.status===0,'legacy blocked workflow state does not trap mutation',legacy.stderr);
34
+ check(/marked BLOCKED/i.test(legacy.stderr),'legacy blocked state is surfaced as reminder',legacy.stderr);
35
+ fs.writeFileSync(path.join(dir,'workflow-run.json'),'{broken');
36
+ const corrupt=runGate('Write',linked,{file_path:path.join(linked,'corrupt.js'),content:'x'});
37
+ check(corrupt.status===0,'corrupt workflow state fails open',corrupt.stderr);
38
+ }finally{fs.rmSync(temp,{recursive:true,force:true});}
39
+ if(failed)process.exit(1);
40
+ console.log('PASS: worktree isolation is guidance, not a cognitive lock');