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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +14 -0
- package/README.md +7 -7
- package/VERIFICATION.md +3 -3
- package/behavioral-evals/PAIRED-BENCHMARK.md +3 -3
- package/behavioral-evals/README.md +38 -32
- package/behavioral-evals/opencode-hardlock-live.js +6 -290
- package/behavioral-evals/opencode-reflection-gate-live.js +342 -0
- package/behavioral-evals/paired-benchmark.js +3 -3
- package/ci/consistency-check.js +20 -0
- package/ci/invariant-routing.test.js +58 -158
- package/ci/mechanism-2a-rule-of-3.test.js +73 -138
- package/ci/mechanism-2ac-atomic-commit-check.test.js +2 -2
- package/ci/mechanism-2af-worktree-isolation.test.js +38 -196
- package/ci/mechanism-2ah-workflow-lifecycle.test.js +20 -831
- package/ci/mechanism-2ai-workspace-fingerprint-batch.test.js +3 -4
- package/ci/mechanism-2b-boundary-guard.test.js +6 -16
- package/ci/mechanism-2e-scope-guard.test.js +8 -24
- package/ci/mechanism-2f-stop-gate.test.js +21 -75
- package/ci/mechanism-2n-opencode-plugin.test.js +13 -28
- package/ci/mechanism-31-opencode-hardlock-evidence.test.js +43 -99
- package/ci/openai-plugin-package.test.js +8 -8
- package/ci/openai-plugin-runtime.test.js +2 -2
- package/ci/router-workflow-plan.test.js +25 -6
- package/ci/self-evolve-loop.test.js +4 -1
- package/ci/workflow-execution-contract.test.js +51 -129
- package/ci/workflow-plan-consumers-edge.test.js +1 -1
- package/ci/workflow-plan-consumers.test.js +6 -6
- package/docs/architecture.md +21 -21
- package/docs/enforcement-locks.md +41 -102
- package/docs/mechanism-first-skill-mesh.md +2 -2
- package/docs/philosophy.md +2 -2
- package/docs/platform-capabilities.md +3 -3
- package/docs/routing.md +14 -14
- package/docs/workflow-runtime.md +80 -110
- package/evals/harness-everything/tasks/positive-tier1.yaml +1 -1
- package/evals/harness-everything/tasks/positive-tier2.yaml +1 -1
- package/evals/harness-everything/tasks/positive-tier3.yaml +1 -1
- package/evals/using-git-worktrees/tasks/positive-trigger.yaml +1 -1
- package/fable-mode/SKILL.md +1 -1
- package/fable-mode/agents/fable-orchestrator.md +7 -6
- package/fable-mode/execution-guardrails/SKILL.md +1 -1
- package/fable-mode/fable-haiku/SKILL.md +1 -1
- package/fable-mode/fable-opus/SKILL.md +1 -1
- package/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/fable-mode/scripts/workflow-plan-consumer.js +4 -3
- package/harness-everything/SKILL.md +11 -11
- package/harness-everything/references/router-workflow-plan.md +2 -2
- package/harness-everything/references/triage-and-tiers.md +11 -11
- package/harness-everything/scripts/kernel-router-core.js +5 -5
- package/harness-everything/scripts/kernel-router.js +16 -21
- package/harness-everything/scripts/router-contract-core.js +1 -1
- package/hooks/hooks.json +10 -23
- package/hooks/scripts/atomic-commit-check.js +1 -1
- package/hooks/scripts/boundary-guard.js +4 -4
- package/hooks/scripts/contract-test.js +8 -11
- package/hooks/scripts/depth-guard.js +2 -2
- package/hooks/scripts/lib/workflow-runtime.js +24 -402
- package/hooks/scripts/rule-of-3-tracker.js +28 -31
- package/hooks/scripts/rule-of-3.js +4 -28
- package/hooks/scripts/state-persist.js +36 -136
- package/hooks/scripts/stop-gate.js +15 -48
- package/hooks/scripts/subagent-scope-guard.js +8 -8
- package/hooks/scripts/workflow-disposition.js +2 -10
- package/hooks/scripts/workflow-gate.js +33 -84
- package/hooks/scripts/workflow-mutation-denied.js +3 -15
- package/hooks/scripts/workflow-stop-gate.js +23 -38
- package/opencode-plugin/README.md +10 -13
- package/opencode-plugin/index.mjs +14 -30
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +3 -2
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/hooks.json +3 -3
- package/plugins/harness-everything/hooks/scripts/atomic-commit-check.js +1 -1
- package/plugins/harness-everything/hooks/scripts/boundary-guard.js +4 -4
- package/plugins/harness-everything/hooks/scripts/contract-test.js +8 -11
- package/plugins/harness-everything/hooks/scripts/depth-guard.js +2 -2
- package/plugins/harness-everything/hooks/scripts/lib/workflow-runtime.js +24 -402
- package/plugins/harness-everything/hooks/scripts/rule-of-3-tracker.js +28 -31
- package/plugins/harness-everything/hooks/scripts/rule-of-3.js +4 -28
- package/plugins/harness-everything/hooks/scripts/state-persist.js +36 -136
- package/plugins/harness-everything/hooks/scripts/stop-gate.js +15 -48
- package/plugins/harness-everything/hooks/scripts/subagent-scope-guard.js +8 -8
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +2 -10
- package/plugins/harness-everything/hooks/scripts/workflow-gate.js +33 -84
- package/plugins/harness-everything/hooks/scripts/workflow-mutation-denied.js +3 -15
- package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +23 -38
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/scripts/lib/advisory-text.js +16 -29
- package/plugins/harness-everything/skills/fable-mode/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/agents/fable-orchestrator.md +7 -6
- package/plugins/harness-everything/skills/fable-mode/execution-guardrails/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-haiku/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-opus/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/fable-sonnet/SKILL.md +1 -1
- package/plugins/harness-everything/skills/fable-mode/scripts/workflow-plan-consumer.js +4 -3
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +11 -11
- package/plugins/harness-everything/skills/harness-everything/references/router-workflow-plan.md +2 -2
- package/plugins/harness-everything/skills/harness-everything/references/triage-and-tiers.md +11 -11
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router-core.js +5 -5
- package/plugins/harness-everything/skills/harness-everything/scripts/kernel-router.js +16 -21
- package/plugins/harness-everything/skills/harness-everything/scripts/router-contract-core.js +1 -1
- package/plugins/harness-everything/skills/using-git-worktrees/SKILL.md +5 -5
- package/plugins/harness-everything/skills/zoom-out/SKILL.md +3 -3
- package/plugins/harness-everything/skills/zoom-out/references/circuit-breaker.md +4 -4
- package/scripts/lib/advisory-text.js +16 -29
- package/using-git-worktrees/SKILL.md +5 -5
- package/zoom-out/SKILL.md +3 -3
- 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
|
-
|
|
9
|
-
);
|
|
10
|
-
const failureTrackerHook = (hooksConfig.hooks.PostToolUseFailure || [])
|
|
11
|
-
|
|
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-
|
|
14
|
-
failureTrackerHook &&
|
|
15
|
-
failureTrackerHook
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
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-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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-
|
|
64
|
-
|
|
65
|
-
|
|
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.
|
|
72
|
-
const
|
|
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-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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: '
|
|
115
|
-
error: '
|
|
88
|
+
tool_input: { command: 'node flaky.js' },
|
|
89
|
+
error: 'Error: flaky failure',
|
|
116
90
|
session_id: helper.SESSION_ID,
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
helper.
|
|
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: '
|
|
95
|
+
hook_event_name: 'PostToolUse',
|
|
130
96
|
tool_name: 'Bash',
|
|
131
|
-
tool_input: { command: '
|
|
132
|
-
|
|
97
|
+
tool_input: { command: 'git status --short' },
|
|
98
|
+
tool_response: { exitCode: 0, stdout: '' },
|
|
133
99
|
session_id: helper.SESSION_ID,
|
|
134
100
|
});
|
|
135
|
-
|
|
136
|
-
helper.check(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
5
|
-
|
|
6
|
-
const path
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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');
|