harness-everything 0.15.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +23 -0
- package/README.md +7 -7
- package/VERIFICATION.md +3 -3
- package/bin/cli.js +17 -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-2al-codex-hooks-compat.test.js +286 -0
- 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/openai-plugin-package.test.js +8 -8
- package/ci/openai-plugin-runtime.test.js +2 -2
- package/ci/router-workflow-plan.test.js +6 -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 +19 -3
- package/docs/platform-compatibility.json +4 -4
- 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/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/triage-and-tiers.md +11 -11
- package/harness-everything/scripts/kernel-router-core.js +5 -5
- package/harness-everything/scripts/kernel-router.js +15 -17
- 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 +9 -10
- package/opencode-plugin/index.mjs +14 -30
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +4 -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/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/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 +15 -17
- 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/scripts/codex-hooks-compat.js +521 -0
- package/scripts/lib/advisory-text.js +16 -29
- 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
|
|
206
|
-
|
|
207
|
-
|
|
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 {
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const compat = require('../scripts/codex-hooks-compat');
|
|
10
|
+
|
|
11
|
+
let failed = 0;
|
|
12
|
+
function check(condition, message, detail = '') {
|
|
13
|
+
if (condition) console.log(' PASS ' + message);
|
|
14
|
+
else {
|
|
15
|
+
failed++;
|
|
16
|
+
console.error(' FAIL ' + message + (detail ? ' — ' + detail : ''));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function read(file) {
|
|
21
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findHarnessEntries(config, runtimeDir) {
|
|
25
|
+
const out = [];
|
|
26
|
+
for (const [event, entries] of Object.entries(config.hooks || {})) {
|
|
27
|
+
for (const entry of entries || []) {
|
|
28
|
+
if (JSON.stringify(entry).toLowerCase().includes(path.resolve(runtimeDir).replace(/\\/g, '\\\\').toLowerCase()) ||
|
|
29
|
+
JSON.stringify(entry).replace(/\\\\/g, '/').toLowerCase().includes(path.resolve(runtimeDir).replace(/\\/g, '/').toLowerCase())) {
|
|
30
|
+
out.push({ event, entry });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('\n[2al] Codex user-hook compatibility fallback (#122)...');
|
|
38
|
+
|
|
39
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-codex-hooks-compat-'));
|
|
40
|
+
const codexHome = path.join(root, '.codex');
|
|
41
|
+
fs.mkdirSync(codexHome, { recursive: true });
|
|
42
|
+
const hooksFile = path.join(codexHome, 'hooks.json');
|
|
43
|
+
const configFile = path.join(codexHome, 'config.toml');
|
|
44
|
+
const userHook = {
|
|
45
|
+
matcher: 'Bash',
|
|
46
|
+
hooks: [{
|
|
47
|
+
type: 'command',
|
|
48
|
+
command: 'node "/user/policy.js"',
|
|
49
|
+
timeout: 7,
|
|
50
|
+
statusMessage: 'User policy',
|
|
51
|
+
}],
|
|
52
|
+
};
|
|
53
|
+
const original = {
|
|
54
|
+
description: 'user-owned metadata',
|
|
55
|
+
hooks: {
|
|
56
|
+
PreToolUse: [userHook],
|
|
57
|
+
Stop: [{
|
|
58
|
+
hooks: [{ type: 'command', command: 'node "/user/stop.js"', timeout: 5 }],
|
|
59
|
+
}],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
fs.writeFileSync(hooksFile, JSON.stringify(original, null, 4) + '\n', 'utf8');
|
|
63
|
+
fs.writeFileSync(configFile, 'model = "test"\n# user trust/config state\n', 'utf8');
|
|
64
|
+
const configBefore = fs.readFileSync(configFile, 'utf8');
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const installed = compat.install({ codexHome });
|
|
68
|
+
check(installed.status === 'installed' && installed.entries > 0,
|
|
69
|
+
'explicit install materializes compatibility entries');
|
|
70
|
+
check(installed.trustChanged === false && fs.readFileSync(configFile, 'utf8') === configBefore,
|
|
71
|
+
'install never changes Codex trust/config.toml state');
|
|
72
|
+
|
|
73
|
+
const state = compat.status({ codexHome });
|
|
74
|
+
check(state.installed && state.healthy && state.exactEntriesPresent === state.expectedEntries,
|
|
75
|
+
'status verifies exact installed entry hashes and runtime tree');
|
|
76
|
+
|
|
77
|
+
const config = read(hooksFile);
|
|
78
|
+
check(config.description === original.description &&
|
|
79
|
+
JSON.stringify(config.hooks.PreToolUse[0]) === JSON.stringify(userHook) &&
|
|
80
|
+
config.hooks.Stop.some(entry => entry.hooks?.[0]?.command === 'node "/user/stop.js"'),
|
|
81
|
+
'existing unrelated user hooks and top-level metadata are preserved semantically');
|
|
82
|
+
|
|
83
|
+
const manifest = read(path.join(codexHome, 'harness-everything', 'compat-hooks', 'manifest.json'));
|
|
84
|
+
const harnessEntries = findHarnessEntries(config, manifest.runtimeDir);
|
|
85
|
+
check(harnessEntries.length === manifest.entries.length,
|
|
86
|
+
'every compatibility hook command resolves to the Harness-owned runtime copy');
|
|
87
|
+
check(!JSON.stringify(harnessEntries).includes('${PLUGIN_ROOT}') &&
|
|
88
|
+
!JSON.stringify(harnessEntries).includes('${CLAUDE_PLUGIN_ROOT}') &&
|
|
89
|
+
!JSON.stringify(harnessEntries).includes('%PLUGIN_ROOT%'),
|
|
90
|
+
'compatibility entries use literal absolute runtime paths without shell-specific plugin expansion');
|
|
91
|
+
check(fs.existsSync(path.join(manifest.runtimeDir, 'hooks', 'scripts', 'codex-action-gate-pre.js')) &&
|
|
92
|
+
fs.existsSync(path.join(manifest.runtimeDir, 'skills', 'harness-everything', 'scripts', 'kernel-router.js')),
|
|
93
|
+
'compatibility runtime is self-contained for hook and router dependencies');
|
|
94
|
+
|
|
95
|
+
const countsBefore = Object.fromEntries(Object.entries(config.hooks).map(([event, entries]) => [event, entries.length]));
|
|
96
|
+
const updated = compat.install({ codexHome });
|
|
97
|
+
const configAfterUpdate = read(hooksFile);
|
|
98
|
+
const countsAfter = Object.fromEntries(Object.entries(configAfterUpdate.hooks).map(([event, entries]) => [event, entries.length]));
|
|
99
|
+
check(updated.status === 'updated' && JSON.stringify(countsAfter) === JSON.stringify(countsBefore),
|
|
100
|
+
'reinstall/update is idempotent and does not duplicate hook entries');
|
|
101
|
+
|
|
102
|
+
configAfterUpdate.hooks.PostToolUse = configAfterUpdate.hooks.PostToolUse || [];
|
|
103
|
+
const addedUserHook = { hooks: [{ type: 'command', command: 'node "/user/later.js"', timeout: 3 }] };
|
|
104
|
+
configAfterUpdate.hooks.PostToolUse.unshift(addedUserHook);
|
|
105
|
+
fs.writeFileSync(hooksFile, JSON.stringify(configAfterUpdate, null, 2) + '\n', 'utf8');
|
|
106
|
+
compat.install({ codexHome });
|
|
107
|
+
check(read(hooksFile).hooks.PostToolUse.some(entry => entry.hooks?.[0]?.command === 'node "/user/later.js"'),
|
|
108
|
+
'update preserves user hooks added after the initial compatibility install');
|
|
109
|
+
|
|
110
|
+
const beforeTamper = fs.readFileSync(hooksFile, 'utf8');
|
|
111
|
+
const tampered = read(hooksFile);
|
|
112
|
+
const currentManifest = read(path.join(codexHome, 'harness-everything', 'compat-hooks', 'manifest.json'));
|
|
113
|
+
let changed = false;
|
|
114
|
+
for (const [event, entries] of Object.entries(tampered.hooks)) {
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
if (!changed && JSON.stringify(entry).replace(/\\\\/g, '/').toLowerCase()
|
|
117
|
+
.includes(path.resolve(currentManifest.runtimeDir).replace(/\\/g, '/').toLowerCase())) {
|
|
118
|
+
entry.hooks[0].statusMessage = 'user modified this installed Harness hook';
|
|
119
|
+
changed = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
fs.writeFileSync(hooksFile, JSON.stringify(tampered, null, 2) + '\n', 'utf8');
|
|
124
|
+
const tamperedBytes = fs.readFileSync(hooksFile, 'utf8');
|
|
125
|
+
let updateRefused = false;
|
|
126
|
+
try { compat.install({ codexHome }); } catch (error) { updateRefused = /modified/.test(error.message); }
|
|
127
|
+
check(updateRefused && fs.readFileSync(hooksFile, 'utf8') === tamperedBytes,
|
|
128
|
+
'modified Harness compatibility entry makes update fail closed without rewriting user config');
|
|
129
|
+
|
|
130
|
+
let uninstallRefused = false;
|
|
131
|
+
try { compat.uninstall({ codexHome }); } catch (error) { uninstallRefused = /modified/.test(error.message); }
|
|
132
|
+
check(uninstallRefused && fs.readFileSync(hooksFile, 'utf8') === tamperedBytes,
|
|
133
|
+
'modified Harness compatibility entry makes uninstall fail closed rather than deleting user edits');
|
|
134
|
+
|
|
135
|
+
// Restore the exact generated config by starting from the pre-tamper bytes.
|
|
136
|
+
fs.writeFileSync(hooksFile, beforeTamper, 'utf8');
|
|
137
|
+
compat.install({ codexHome });
|
|
138
|
+
const removed = compat.uninstall({ codexHome });
|
|
139
|
+
const finalConfig = read(hooksFile);
|
|
140
|
+
check(removed.status === 'uninstalled' && removed.removed > 0 && removed.trustChanged === false,
|
|
141
|
+
'explicit uninstall removes exact Harness-owned entries without changing trust state');
|
|
142
|
+
check(finalConfig.description === original.description &&
|
|
143
|
+
finalConfig.hooks.PreToolUse.some(entry => entry.hooks?.[0]?.command === 'node "/user/policy.js"') &&
|
|
144
|
+
finalConfig.hooks.Stop.some(entry => entry.hooks?.[0]?.command === 'node "/user/stop.js"') &&
|
|
145
|
+
finalConfig.hooks.PostToolUse.some(entry => entry.hooks?.[0]?.command === 'node "/user/later.js"'),
|
|
146
|
+
'uninstall preserves unrelated pre-existing and later-added user hooks');
|
|
147
|
+
check(fs.readFileSync(configFile, 'utf8') === configBefore,
|
|
148
|
+
'uninstall leaves config.toml/trust state byte-identical');
|
|
149
|
+
check(!fs.existsSync(path.join(codexHome, 'harness-everything', 'compat-hooks', 'manifest.json')),
|
|
150
|
+
'uninstall removes Harness compatibility ownership manifest');
|
|
151
|
+
|
|
152
|
+
const emptyHome = path.join(root, 'empty-codex');
|
|
153
|
+
fs.mkdirSync(emptyHome, { recursive: true });
|
|
154
|
+
compat.install({ codexHome: emptyHome });
|
|
155
|
+
check(fs.existsSync(path.join(emptyHome, 'hooks.json')),
|
|
156
|
+
'compatibility install can create a new hooks.json when none exists');
|
|
157
|
+
compat.uninstall({ codexHome: emptyHome });
|
|
158
|
+
check(!fs.existsSync(path.join(emptyHome, 'hooks.json')),
|
|
159
|
+
'uninstall removes hooks.json only when Harness created it and no user entries remain');
|
|
160
|
+
|
|
161
|
+
const duplicateHome = path.join(root, 'duplicate-codex');
|
|
162
|
+
fs.mkdirSync(duplicateHome, { recursive: true });
|
|
163
|
+
compat.install({ codexHome: duplicateHome });
|
|
164
|
+
const duplicateConfig = read(path.join(duplicateHome, 'hooks.json'));
|
|
165
|
+
duplicateConfig.hooks.PreToolUse.push(JSON.parse(JSON.stringify(duplicateConfig.hooks.PreToolUse[0])));
|
|
166
|
+
fs.writeFileSync(path.join(duplicateHome, 'hooks.json'), JSON.stringify(duplicateConfig, null, 2) + '\n', 'utf8');
|
|
167
|
+
const duplicateBytes = fs.readFileSync(path.join(duplicateHome, 'hooks.json'), 'utf8');
|
|
168
|
+
let duplicateRefused = false;
|
|
169
|
+
try { compat.uninstall({ codexHome: duplicateHome }); } catch (error) { duplicateRefused = /modified/.test(error.message); }
|
|
170
|
+
check(duplicateRefused && fs.readFileSync(path.join(duplicateHome, 'hooks.json'), 'utf8') === duplicateBytes,
|
|
171
|
+
'extra identical Harness hook is treated as user config drift, not silently deleted');
|
|
172
|
+
|
|
173
|
+
const removedConfigHome = path.join(root, 'removed-config-codex');
|
|
174
|
+
fs.mkdirSync(removedConfigHome, { recursive: true });
|
|
175
|
+
fs.writeFileSync(path.join(removedConfigHome, 'hooks.json'), JSON.stringify(original, null, 2) + '\n', 'utf8');
|
|
176
|
+
compat.install({ codexHome: removedConfigHome });
|
|
177
|
+
fs.rmSync(path.join(removedConfigHome, 'hooks.json'), { force: true });
|
|
178
|
+
const removedConfigResult = compat.uninstall({ codexHome: removedConfigHome });
|
|
179
|
+
check(removedConfigResult.status === 'uninstalled' && !fs.existsSync(path.join(removedConfigHome, 'hooks.json')),
|
|
180
|
+
'uninstall never recreates a user hooks.json that was removed after compatibility install');
|
|
181
|
+
|
|
182
|
+
const preexistingHome = path.join(root, 'preexisting-identical-codex');
|
|
183
|
+
fs.mkdirSync(preexistingHome, { recursive: true });
|
|
184
|
+
const sourceHash = compat.treeHash(compat.SOURCE_PLUGIN_ROOT);
|
|
185
|
+
const preexistingPaths = compat.pathsFor(preexistingHome, sourceHash);
|
|
186
|
+
const preexistingEntries = compat.sourceHookEntries(preexistingPaths.runtimeDir);
|
|
187
|
+
const preexistingHooks = {};
|
|
188
|
+
for (const record of preexistingEntries) {
|
|
189
|
+
preexistingHooks[record.event] = preexistingHooks[record.event] || [];
|
|
190
|
+
preexistingHooks[record.event].push(JSON.parse(JSON.stringify(record.entry)));
|
|
191
|
+
}
|
|
192
|
+
fs.writeFileSync(path.join(preexistingHome, 'hooks.json'),
|
|
193
|
+
JSON.stringify({ hooks: preexistingHooks }, null, 2) + '\n', 'utf8');
|
|
194
|
+
const preexistingBefore = fs.readFileSync(path.join(preexistingHome, 'hooks.json'), 'utf8');
|
|
195
|
+
const preexistingInstall = compat.install({ codexHome: preexistingHome });
|
|
196
|
+
const preexistingManifest = read(path.join(preexistingHome, 'harness-everything', 'compat-hooks', 'manifest.json'));
|
|
197
|
+
check(preexistingInstall.status === 'installed' && preexistingManifest.entries.length === 0,
|
|
198
|
+
'byte-identical pre-existing user hooks are reused but never recorded as Harness-owned');
|
|
199
|
+
const preexistingRemoved = compat.uninstall({ codexHome: preexistingHome });
|
|
200
|
+
check(preexistingRemoved.removed === 0 &&
|
|
201
|
+
fs.readFileSync(path.join(preexistingHome, 'hooks.json'), 'utf8') === preexistingBefore,
|
|
202
|
+
'uninstall preserves byte-identical hooks that predated compatibility install');
|
|
203
|
+
|
|
204
|
+
const forgedManifestHome = path.join(root, 'forged-manifest-codex');
|
|
205
|
+
fs.mkdirSync(forgedManifestHome, { recursive: true });
|
|
206
|
+
fs.writeFileSync(path.join(forgedManifestHome, 'hooks.json'),
|
|
207
|
+
JSON.stringify({ hooks: { PreToolUse: [userHook] } }, null, 2) + '\n', 'utf8');
|
|
208
|
+
compat.install({ codexHome: forgedManifestHome });
|
|
209
|
+
const forgedManifestPath = path.join(
|
|
210
|
+
forgedManifestHome, 'harness-everything', 'compat-hooks', 'manifest.json');
|
|
211
|
+
const forgedManifest = read(forgedManifestPath);
|
|
212
|
+
forgedManifest.entries.push({
|
|
213
|
+
event: 'PreToolUse',
|
|
214
|
+
hash: compat.entryHash(userHook),
|
|
215
|
+
entry: userHook,
|
|
216
|
+
});
|
|
217
|
+
fs.writeFileSync(forgedManifestPath, JSON.stringify(forgedManifest, null, 2) + '\n', 'utf8');
|
|
218
|
+
const forgedHooksBefore = fs.readFileSync(path.join(forgedManifestHome, 'hooks.json'), 'utf8');
|
|
219
|
+
let forgedManifestRefused = false;
|
|
220
|
+
try { compat.uninstall({ codexHome: forgedManifestHome }); }
|
|
221
|
+
catch (error) { forgedManifestRefused = /corrupt/.test(error.message); }
|
|
222
|
+
check(forgedManifestRefused &&
|
|
223
|
+
fs.readFileSync(path.join(forgedManifestHome, 'hooks.json'), 'utf8') === forgedHooksBefore,
|
|
224
|
+
'forged manifest ownership fails closed without deleting a user hook');
|
|
225
|
+
|
|
226
|
+
const failedInstallHome = path.join(root, 'failed-install-codex');
|
|
227
|
+
fs.mkdirSync(failedInstallHome, { recursive: true });
|
|
228
|
+
const failedInstallHooks = path.join(failedInstallHome, 'hooks.json');
|
|
229
|
+
fs.writeFileSync(failedInstallHooks,
|
|
230
|
+
JSON.stringify({ description: 'must survive', hooks: { PreToolUse: [userHook] } }, null, 4) + '\n',
|
|
231
|
+
'utf8');
|
|
232
|
+
const failedInstallBefore = fs.readFileSync(failedInstallHooks, 'utf8');
|
|
233
|
+
const failedManifestPath = path.join(
|
|
234
|
+
failedInstallHome, 'harness-everything', 'compat-hooks', 'manifest.json');
|
|
235
|
+
const realRenameSync = fs.renameSync;
|
|
236
|
+
let failedInstallRefused = false;
|
|
237
|
+
fs.renameSync = function injectedManifestFailure(source, destination) {
|
|
238
|
+
if (path.resolve(destination) === path.resolve(failedManifestPath)) {
|
|
239
|
+
throw new Error('injected manifest commit failure');
|
|
240
|
+
}
|
|
241
|
+
return realRenameSync(source, destination);
|
|
242
|
+
};
|
|
243
|
+
try { compat.install({ codexHome: failedInstallHome }); }
|
|
244
|
+
catch (error) { failedInstallRefused = /injected manifest commit failure/.test(error.message); }
|
|
245
|
+
finally { fs.renameSync = realRenameSync; }
|
|
246
|
+
check(failedInstallRefused &&
|
|
247
|
+
fs.readFileSync(failedInstallHooks, 'utf8') === failedInstallBefore &&
|
|
248
|
+
!fs.existsSync(failedManifestPath),
|
|
249
|
+
'failed manifest commit rolls hooks.json back byte-identically');
|
|
250
|
+
|
|
251
|
+
const linkedOwnerHome = path.join(root, 'linked-owner-codex');
|
|
252
|
+
const linkedOwnerOutside = path.join(root, 'linked-owner-outside');
|
|
253
|
+
fs.mkdirSync(path.join(linkedOwnerHome, 'harness-everything'), { recursive: true });
|
|
254
|
+
fs.mkdirSync(linkedOwnerOutside, { recursive: true });
|
|
255
|
+
fs.symlinkSync(
|
|
256
|
+
linkedOwnerOutside,
|
|
257
|
+
path.join(linkedOwnerHome, 'harness-everything', 'compat-hooks'),
|
|
258
|
+
process.platform === 'win32' ? 'junction' : 'dir'
|
|
259
|
+
);
|
|
260
|
+
let linkedOwnerRefused = false;
|
|
261
|
+
try { compat.install({ codexHome: linkedOwnerHome }); }
|
|
262
|
+
catch (error) { linkedOwnerRefused = /linked|outside/.test(error.message); }
|
|
263
|
+
check(linkedOwnerRefused && fs.readdirSync(linkedOwnerOutside).length === 0,
|
|
264
|
+
'linked compatibility owner path cannot redirect writes outside Codex home');
|
|
265
|
+
|
|
266
|
+
const malformedHome = path.join(root, 'malformed-codex');
|
|
267
|
+
fs.mkdirSync(malformedHome, { recursive: true });
|
|
268
|
+
const malformed = path.join(malformedHome, 'hooks.json');
|
|
269
|
+
fs.writeFileSync(malformed, '{ this is not json', 'utf8');
|
|
270
|
+
const malformedBefore = fs.readFileSync(malformed, 'utf8');
|
|
271
|
+
let malformedRefused = false;
|
|
272
|
+
try { compat.install({ codexHome: malformedHome }); } catch (_) { malformedRefused = true; }
|
|
273
|
+
check(malformedRefused && fs.readFileSync(malformed, 'utf8') === malformedBefore,
|
|
274
|
+
'malformed user hooks.json fails closed without replacement');
|
|
275
|
+
|
|
276
|
+
const source = read(path.join(compat.SOURCE_PLUGIN_ROOT, 'hooks', 'hooks.json'));
|
|
277
|
+
const sourceCount = Object.values(source.hooks).reduce((sum, entries) => sum + entries.length, 0);
|
|
278
|
+
check(sourceCount === installed.entries,
|
|
279
|
+
'fallback derives from the current packaged hook manifest instead of maintaining a second manual hook inventory');
|
|
280
|
+
} finally {
|
|
281
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
console.log('\n' + (failed === 0 ? 'PASS' : 'FAIL') +
|
|
285
|
+
': #122 Codex compatibility fallback (' + failed + ' failure' + (failed === 1 ? '' : 's') + ')');
|
|
286
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -1,16 +1,6 @@
|
|
|
1
|
-
const fs
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
helper.
|
|
8
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
);
|
|
21
|
-
|
|
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 ===
|
|
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
|
|
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.
|
|
338
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 ===
|
|
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
|
|
386
|
-
corruptStateBlocked
|
|
387
|
-
corruptStateMessage || '
|
|
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
|
|
166
|
-
assert.match(hookText, /workflow-stop-gate\.js/, 'plugin hooks must package workflow completion
|
|
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
|
|
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:
|
|
198
|
-
assert.match(first.stdout, /WORKFLOW
|
|
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, /
|
|
201
|
-
assert.doesNotMatch(first.stdout, /
|
|
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
|
|
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,
|
|
173
|
-
assert.match(subagentStop.stderr, /Subagent Scope
|
|
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
|
|
79
|
-
check(plan.limits.maxRevisionRounds === WORKFLOW_MAX_REVISION_ROUNDS, 'workflow exposes
|
|
80
|
-
check(plan.limits.maxReplans === FABLE_MAX_REPLANS, 'workflow exposes
|
|
81
|
-
check(plan.limits.maxWorkers === FABLE_MAX_WORKERS, 'workflow exposes
|
|
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-
|
|
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-
|
|
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 ===
|
|
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,
|