harness-everything 0.14.0 → 0.16.0
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/CHANGELOG.md +32 -0
- package/bin/cli.js +17 -0
- package/ci/contract-integrity.test.js +155 -1
- package/ci/mechanism-2al-codex-hooks-compat.test.js +286 -0
- package/contract-integrity/scripts/audit.js +225 -2
- package/docs/platform-capabilities.md +16 -0
- package/docs/platform-compatibility.json +4 -4
- 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/contract-integrity/scripts/audit.js +225 -2
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/verification-loop/SKILL.md +2 -2
- package/plugins/harness-everything/skills/verification-loop/references/verification-phases.md +9 -1
- package/scripts/codex-hooks-compat.js +521 -0
- package/verification-loop/SKILL.md +2 -2
- package/verification-loop/references/verification-phases.md +9 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "harness-everything",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.16.0",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Miya Daniel"
|
|
16
16
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-everything",
|
|
3
3
|
"description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.16.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Miya Daniel"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.16.0](https://github.com/dyphn1/Harness-everything/compare/v0.15.0...v0.16.0) (2026-09-20)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* **codex:** add explicit hooks compatibility installer ([3fb622b](https://github.com/dyphn1/Harness-everything/commit/3fb622b57eef3a9ee5110529bcd2cdc8c81b6a75))
|
|
10
|
+
* **codex:** expose explicit hook fallback CLI ([83e4cf1](https://github.com/dyphn1/Harness-everything/commit/83e4cf1eadd77585d4681bb73acbf360efd63509))
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* **codex:** contain compatibility runtime paths ([1ffcc0d](https://github.com/dyphn1/Harness-everything/commit/1ffcc0d984e7dbfd2dd987a03b189fb950e83de8))
|
|
15
|
+
* **codex:** do not recreate removed hooks config ([ee62a1d](https://github.com/dyphn1/Harness-everything/commit/ee62a1d9f466a2e585e4737576a7289ae12087d5))
|
|
16
|
+
* **codex:** fail closed on duplicate compat hooks ([29920d0](https://github.com/dyphn1/Harness-everything/commit/29920d07e76a73c84c35440b60453c3e0b648b29))
|
|
17
|
+
* **codex:** harden compatibility hook ownership ([3ff5c14](https://github.com/dyphn1/Harness-everything/commit/3ff5c1456e6c44398ef661763057c93bcfcbb57c))
|
|
18
|
+
* **codex:** record only inserted hook ownership ([90e0c9b](https://github.com/dyphn1/Harness-everything/commit/90e0c9bbfd4672955473d33f2e33832f760bf294))
|
|
19
|
+
|
|
20
|
+
## [0.15.0](https://github.com/dyphn1/Harness-everything/compare/v0.14.0...v0.15.0) (2026-09-20)
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **contract:** add freshness provenance helpers ([865fc0b](https://github.com/dyphn1/Harness-everything/commit/865fc0bdb111feff865c3a85767fc587aecbd11a))
|
|
25
|
+
* **contract:** emit requirement-scoped repair guidance ([cbd33ce](https://github.com/dyphn1/Harness-everything/commit/cbd33ce18fb8575671056bd4b2701bd0138baa96))
|
|
26
|
+
* **contract:** reject stale completion reports ([79ea49e](https://github.com/dyphn1/Harness-everything/commit/79ea49e40f110fe062ac714519597e8a23a6ab5b))
|
|
27
|
+
* **contract:** sync freshness audit runtime ([c5d0381](https://github.com/dyphn1/Harness-everything/commit/c5d038163b7ff2dc14e11bd4733e140e85e39f55))
|
|
28
|
+
|
|
29
|
+
### Bug Fixes
|
|
30
|
+
|
|
31
|
+
* **contract:** harden freshness evidence boundaries ([33ebbda](https://github.com/dyphn1/Harness-everything/commit/33ebbdafb2b63c4ece86ee81c14c4fcc6d33907f))
|
|
32
|
+
* **contract:** launch npm probes cross-platform ([9ca5315](https://github.com/dyphn1/Harness-everything/commit/9ca5315cb30e8f03b08fb1f8789a141ea1179af4))
|
|
33
|
+
* **contract:** launch npm probes cross-platform ([2cad76e](https://github.com/dyphn1/Harness-everything/commit/2cad76e217af136d934946f05c740d816e051acd))
|
|
34
|
+
* **contract:** require complete freshness provenance ([5b6d717](https://github.com/dyphn1/Harness-everything/commit/5b6d717487fce37dcd1d1e8cbe8b037597035103))
|
|
35
|
+
* **contract:** require complete freshness provenance ([d94e256](https://github.com/dyphn1/Harness-everything/commit/d94e2562c54d615d223a8572fd4d468e017a5807))
|
|
36
|
+
|
|
5
37
|
## [0.14.0](https://github.com/dyphn1/Harness-everything/compare/v0.13.0...v0.14.0) (2026-09-20)
|
|
6
38
|
|
|
7
39
|
### Features
|
package/bin/cli.js
CHANGED
|
@@ -37,6 +37,9 @@ switch (command) {
|
|
|
37
37
|
case 'verify-install':
|
|
38
38
|
runInstallVerification(args.slice(1));
|
|
39
39
|
break;
|
|
40
|
+
case 'codex-hooks':
|
|
41
|
+
runCodexHooksCompat(args.slice(1));
|
|
42
|
+
break;
|
|
40
43
|
default:
|
|
41
44
|
console.error(`[Error] Unknown command: "${command}"`);
|
|
42
45
|
showHelp();
|
|
@@ -97,6 +100,10 @@ Commands:
|
|
|
97
100
|
work; hosts with UserPromptSubmit hooks receive it automatically.
|
|
98
101
|
verify-install Compare installed Harness manifests and skill trees with
|
|
99
102
|
this package source; stale versions or missing files fail.
|
|
103
|
+
codex-hooks Explicit Codex user-hook compatibility fallback for hosts
|
|
104
|
+
that fail to mount enabled plugin-bundled hooks.
|
|
105
|
+
Usage: harness codex-hooks <install|status|uninstall>
|
|
106
|
+
Does not trust hooks or modify Codex config.toml.
|
|
100
107
|
verify Run the pre-delivery verification gate (lint/test from the
|
|
101
108
|
nearest package.json). Exits non-zero if checks fail. This is
|
|
102
109
|
the explicit stand-in for a hard stop gate on platforms where
|
|
@@ -162,3 +169,13 @@ function runInstallVerification(verifyArgs) {
|
|
|
162
169
|
const result = spawnSync('node', [script, ...verifyArgs], { stdio: 'inherit' });
|
|
163
170
|
process.exit(result.status === null ? 1 : result.status);
|
|
164
171
|
}
|
|
172
|
+
|
|
173
|
+
function runCodexHooksCompat(compatArgs) {
|
|
174
|
+
const script = path.resolve(__dirname, '..', 'scripts', 'codex-hooks-compat.js');
|
|
175
|
+
if (!fs.existsSync(script)) {
|
|
176
|
+
console.error('[Error] Codex hooks compatibility script not found.');
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
const result = spawnSync('node', [script, ...compatArgs], { stdio: 'inherit' });
|
|
180
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
181
|
+
}
|
|
@@ -265,5 +265,159 @@ for (const relative of [
|
|
|
265
265
|
'Phase 2 OpenAI package mirror matches canonical: ' + relative);
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
|
|
268
|
+
// Phase 4: reports carry actionable requirement-scoped repair guidance and
|
|
269
|
+
// content fingerprints so stale completion evidence cannot be reused.
|
|
270
|
+
check(driftReport.repairGuidance.some(item =>
|
|
271
|
+
item.requirementId === 'REQ-001' && item.code === 'update-living-spec' &&
|
|
272
|
+
item.sourceRef === 'docs/specs/api.md#REQ-001'),
|
|
273
|
+
'stale-spec drift emits requirement-scoped living-spec repair guidance');
|
|
274
|
+
check(paddedReport.repairGuidance.some(item =>
|
|
275
|
+
item.requirementId === 'REQ-001' && item.code === 'strengthen-weak-oracle' && item.priority === 'BLOCKER'),
|
|
276
|
+
'surviving required probe emits BLOCKER weak-oracle guidance');
|
|
277
|
+
check(/Repair Guidance/.test(markdown(driftReport)) && /REQ-001/.test(markdown(driftReport)),
|
|
278
|
+
'Markdown keeps repair guidance concise and tied to requirement identity');
|
|
279
|
+
|
|
280
|
+
const freshRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-contract-freshness-'));
|
|
281
|
+
try {
|
|
282
|
+
const workspace = path.join(freshRoot, 'workspace');
|
|
283
|
+
fs.mkdirSync(workspace, { recursive: true });
|
|
284
|
+
for (const artifact of baseline.artifacts) {
|
|
285
|
+
const file = path.join(workspace, artifact.path);
|
|
286
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
287
|
+
fs.writeFileSync(file, artifact.id + ': fixture content\n', 'utf8');
|
|
288
|
+
}
|
|
289
|
+
const tracePath = path.join(freshRoot, 'trace.json');
|
|
290
|
+
const tddPath = path.join(freshRoot, 'tdd.json');
|
|
291
|
+
const reportPath = path.join(freshRoot, 'report.json');
|
|
292
|
+
fs.writeFileSync(tracePath, JSON.stringify(baseline), 'utf8');
|
|
293
|
+
fs.writeFileSync(tddPath, JSON.stringify(baselineTdd), 'utf8');
|
|
294
|
+
|
|
295
|
+
const unboundReportPath = path.join(freshRoot, 'report-unbound.json');
|
|
296
|
+
const unboundAudit = spawnSync(process.execPath, [
|
|
297
|
+
path.join(ROOT, 'contract-integrity/scripts/audit.js'),
|
|
298
|
+
tracePath,
|
|
299
|
+
'--tdd-evidence', tddPath,
|
|
300
|
+
'--output', unboundReportPath,
|
|
301
|
+
], { encoding: 'utf8' });
|
|
302
|
+
check(unboundAudit.status === 0, 'strict audit may emit a PASS report without workspace binding for immediate inspection');
|
|
303
|
+
const emptyWorkspace = path.join(freshRoot, 'empty-workspace');
|
|
304
|
+
fs.mkdirSync(emptyWorkspace, { recursive: true });
|
|
305
|
+
const unboundFreshness = spawnSync(process.execPath, [
|
|
306
|
+
path.join(ROOT, 'contract-integrity/scripts/audit.js'),
|
|
307
|
+
tracePath,
|
|
308
|
+
'--tdd-evidence', tddPath,
|
|
309
|
+
'--workspace', emptyWorkspace,
|
|
310
|
+
'--verify-fresh', unboundReportPath,
|
|
311
|
+
], { encoding: 'utf8' });
|
|
312
|
+
check(unboundFreshness.status === 1 &&
|
|
313
|
+
/report-workspace-unbound/.test(unboundFreshness.stderr) &&
|
|
314
|
+
/artifact-fingerprint-missing:/.test(unboundFreshness.stderr),
|
|
315
|
+
'report created without workspace fingerprints cannot later authorize freshness against a supplied workspace');
|
|
316
|
+
|
|
317
|
+
const audit = spawnSync(process.execPath, [
|
|
318
|
+
path.join(ROOT, 'contract-integrity/scripts/audit.js'),
|
|
319
|
+
tracePath,
|
|
320
|
+
'--tdd-evidence', tddPath,
|
|
321
|
+
'--workspace', workspace,
|
|
322
|
+
'--output', reportPath,
|
|
323
|
+
], { encoding: 'utf8' });
|
|
324
|
+
check(audit.status === 0, 'strict audit with existing workspace artifacts produces fresh PASS report', audit.stderr);
|
|
325
|
+
const freshReport = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
|
326
|
+
check(freshReport.provenance?.inputs?.trace?.sha256 &&
|
|
327
|
+
freshReport.provenance?.inputs?.tddEvidence?.sha256 &&
|
|
328
|
+
freshReport.provenance?.artifacts?.length === baseline.artifacts.length &&
|
|
329
|
+
freshReport.provenance.artifacts.every(item => item.sha256 && item.missing === false),
|
|
330
|
+
'report provenance records trace/TDD hashes plus current artifact hashes');
|
|
331
|
+
|
|
332
|
+
const verifyFresh = () => spawnSync(process.execPath, [
|
|
333
|
+
path.join(ROOT, 'contract-integrity/scripts/audit.js'),
|
|
334
|
+
tracePath,
|
|
335
|
+
'--tdd-evidence', tddPath,
|
|
336
|
+
'--workspace', workspace,
|
|
337
|
+
'--verify-fresh', reportPath,
|
|
338
|
+
], { encoding: 'utf8' });
|
|
339
|
+
|
|
340
|
+
const current = verifyFresh();
|
|
341
|
+
check(current.status === 0 && /Freshness: PASS/.test(current.stdout),
|
|
342
|
+
'unchanged PASS report can authorize completion');
|
|
343
|
+
|
|
344
|
+
const failingTracePath = path.join(freshRoot, 'trace-failing.json');
|
|
345
|
+
const failingReportPath = path.join(freshRoot, 'report-failing.json');
|
|
346
|
+
const failingTrace = clone(baseline);
|
|
347
|
+
failingTrace.probes[0].status = 'SURVIVED';
|
|
348
|
+
failingTrace.probes[0].failureClass = 'none';
|
|
349
|
+
fs.writeFileSync(failingTracePath, JSON.stringify(failingTrace), 'utf8');
|
|
350
|
+
const failingAudit = spawnSync(process.execPath, [
|
|
351
|
+
path.join(ROOT, 'contract-integrity/scripts/audit.js'),
|
|
352
|
+
failingTracePath,
|
|
353
|
+
'--tdd-evidence', tddPath,
|
|
354
|
+
'--workspace', workspace,
|
|
355
|
+
'--output', failingReportPath,
|
|
356
|
+
], { encoding: 'utf8' });
|
|
357
|
+
check(failingAudit.status === 1, 'surviving required probe produces a reusable FAIL report fixture');
|
|
358
|
+
const forgedReport = JSON.parse(fs.readFileSync(failingReportPath, 'utf8'));
|
|
359
|
+
forgedReport.result = 'PASS';
|
|
360
|
+
forgedReport.completionGate = 'PASS';
|
|
361
|
+
fs.writeFileSync(failingReportPath, JSON.stringify(forgedReport), 'utf8');
|
|
362
|
+
const forgedFreshness = spawnSync(process.execPath, [
|
|
363
|
+
path.join(ROOT, 'contract-integrity/scripts/audit.js'),
|
|
364
|
+
failingTracePath,
|
|
365
|
+
'--tdd-evidence', tddPath,
|
|
366
|
+
'--workspace', workspace,
|
|
367
|
+
'--verify-fresh', failingReportPath,
|
|
368
|
+
], { encoding: 'utf8' });
|
|
369
|
+
check(forgedFreshness.status === 1 && /current-audit-not-pass/.test(forgedFreshness.stderr),
|
|
370
|
+
'freshness verification re-derives the current gate instead of trusting forged PASS fields');
|
|
371
|
+
|
|
372
|
+
const linkedWorkspace = path.join(freshRoot, 'linked-workspace');
|
|
373
|
+
fs.mkdirSync(linkedWorkspace, { recursive: true });
|
|
374
|
+
for (const artifact of baseline.artifacts.filter(item => item.id !== 'IMPL-001')) {
|
|
375
|
+
const file = path.join(linkedWorkspace, artifact.path);
|
|
376
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
377
|
+
fs.writeFileSync(file, artifact.id + ': fixture content\n', 'utf8');
|
|
378
|
+
}
|
|
379
|
+
const outsideImpl = path.join(freshRoot, 'outside-implementation');
|
|
380
|
+
fs.mkdirSync(outsideImpl, { recursive: true });
|
|
381
|
+
fs.writeFileSync(path.join(outsideImpl, 'api.js'), 'IMPL-001: fixture content\n', 'utf8');
|
|
382
|
+
fs.symlinkSync(outsideImpl, path.join(linkedWorkspace, 'src'),
|
|
383
|
+
process.platform === 'win32' ? 'junction' : 'dir');
|
|
384
|
+
const linkedAudit = spawnSync(process.execPath, [
|
|
385
|
+
path.join(ROOT, 'contract-integrity/scripts/audit.js'),
|
|
386
|
+
tracePath,
|
|
387
|
+
'--tdd-evidence', tddPath,
|
|
388
|
+
'--workspace', linkedWorkspace,
|
|
389
|
+
], { encoding: 'utf8' });
|
|
390
|
+
check(linkedAudit.status === 2 && /linked-path: IMPL-001/.test(linkedAudit.stderr),
|
|
391
|
+
'workspace-aware audit rejects artifact paths that resolve through a symlink or junction');
|
|
392
|
+
|
|
393
|
+
const testArtifact = path.join(workspace, 'tests/api.test.js');
|
|
394
|
+
fs.appendFileSync(testArtifact, 'changed after audit\n', 'utf8');
|
|
395
|
+
const staleTestReport = verifyFresh();
|
|
396
|
+
check(staleTestReport.status === 1 &&
|
|
397
|
+
/artifact-changed-since-audit:TEST-001/.test(staleTestReport.stderr),
|
|
398
|
+
'test artifact change invalidates previously passing report');
|
|
399
|
+
fs.writeFileSync(testArtifact, 'TEST-001: fixture content\n', 'utf8');
|
|
400
|
+
|
|
401
|
+
fs.writeFileSync(tddPath, JSON.stringify({ ...baselineTdd, changedAfterAudit: true }), 'utf8');
|
|
402
|
+
const staleEvidence = verifyFresh();
|
|
403
|
+
check(staleEvidence.status === 1 &&
|
|
404
|
+
/tdd-evidence-changed-since-audit/.test(staleEvidence.stderr),
|
|
405
|
+
'#58 evidence change invalidates previously passing report');
|
|
406
|
+
|
|
407
|
+
fs.writeFileSync(tddPath, JSON.stringify(baselineTdd), 'utf8');
|
|
408
|
+
fs.rmSync(path.join(workspace, 'src/api.js'));
|
|
409
|
+
const missingArtifactAudit = spawnSync(process.execPath, [
|
|
410
|
+
path.join(ROOT, 'contract-integrity/scripts/audit.js'),
|
|
411
|
+
tracePath,
|
|
412
|
+
'--tdd-evidence', tddPath,
|
|
413
|
+
'--workspace', workspace,
|
|
414
|
+
], { encoding: 'utf8' });
|
|
415
|
+
check(missingArtifactAudit.status === 2 &&
|
|
416
|
+
/provenance artifact missing from workspace: IMPL-001/.test(missingArtifactAudit.stderr),
|
|
417
|
+
'strict workspace-aware audit fails closed when traced implementation evidence is missing');
|
|
418
|
+
} finally {
|
|
419
|
+
fs.rmSync(freshRoot, { recursive: true, force: true });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
console.log(`\n${failed === 0 ? 'PASS' : 'FAIL'}: #84 contract integrity phase 1-4 mechanism (${failed} failure${failed === 1 ? '' : 's'})`);
|
|
269
423
|
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -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);
|