harness-everything 0.31.0 → 0.31.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/CHANGELOG.md +19 -0
- package/benchmarks/results/live-host/claude-2026-10-13-issue-306/README.md +44 -0
- package/ci/mechanism-2as-workflow-disposition-help.test.js +41 -0
- package/ci/mechanism-35-verification-contract.test.js +185 -0
- package/hooks/scripts/state-persist.js +2 -1
- package/hooks/scripts/workflow-disposition.js +4 -0
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/state-persist.js +2 -1
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +4 -0
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/scripts/lib/verification-contract.js +78 -1
- package/sbom.cdx.json +12 -12
- package/scripts/lib/verification-contract.js +78 -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.31.
|
|
13
|
+
"version": "0.31.2",
|
|
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.31.
|
|
4
|
+
"version": "0.31.2",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Miya Daniel"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.31.2](https://github.com/dyphn1/Harness-everything/compare/v0.31.1...v0.31.2) (2026-10-15)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
* **cli:** support successful workflow disposition help ([2bf55f9](https://github.com/dyphn1/Harness-everything/commit/2bf55f92d9a6ffab5e41152dfb38a5ef01659f05)), closes [#310](https://github.com/dyphn1/Harness-everything/issues/310) [#310](https://github.com/dyphn1/Harness-everything/issues/310)
|
|
10
|
+
|
|
11
|
+
## [0.31.1](https://github.com/dyphn1/Harness-everything/compare/v0.31.0...v0.31.1) (2026-10-13)
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **verify:** pin wrapped checks to stable cd targets and declared cwd ([527b746](https://github.com/dyphn1/Harness-everything/commit/527b746d09db0db1a0b9310f1831bcaf312b1bf0)), closes [#307](https://github.com/dyphn1/Harness-everything/issues/307) [#306](https://github.com/dyphn1/Harness-everything/issues/306)
|
|
16
|
+
* **verify:** recognize safe host cwd wrappers as verification evidence ([de7953f](https://github.com/dyphn1/Harness-everything/commit/de7953f9562bf4d31984cecf053725e1b4219eb2)), closes [#306](https://github.com/dyphn1/Harness-everything/issues/306)
|
|
17
|
+
|
|
5
18
|
## [0.31.0](https://github.com/dyphn1/Harness-everything/compare/v0.30.10...v0.31.0) (2026-10-13)
|
|
6
19
|
|
|
7
20
|
### Features
|
|
@@ -24,9 +37,15 @@ All notable changes to this project are documented in this file.
|
|
|
24
37
|
* **workflow:** reject present non-array required/optional binding declarations before accepting a requirement plan, preserving workflow state on invalid input (#303 review).
|
|
25
38
|
* **test:** isolate verification fixtures from an inherited project-check recursion guard while preserving explicit guard coverage.
|
|
26
39
|
|
|
40
|
+
### Security
|
|
41
|
+
|
|
42
|
+
* **deps:** update the locked `fast-uri` transitive dependency to patched version 3.1.8 (#309).
|
|
43
|
+
|
|
27
44
|
### Bug Fixes
|
|
28
45
|
|
|
46
|
+
* **workflow:** make `workflow-disposition --help` and `-h` print usage successfully (#310).
|
|
29
47
|
* **codex:** keep compatibility hooks and session labels in sync
|
|
48
|
+
* **verify:** recognize a validated in-root host `cd <workspace> && <check>` prelude (Claude Code Windows Bash) as verification evidence for exactly one plain check; chaining, redirection, substitution, and out-of-root targets still never count, and verification evidence now requires a confirmed `PostToolUse` or zero exit (#306). Only CDPATH-stable `cd` targets (absolute or explicit `./`/`../`) are accepted, and a declared `.harness/verify.json` check counts only when run in its declared `cwd` (#307 review).
|
|
30
49
|
|
|
31
50
|
## [0.30.10](https://github.com/dyphn1/Harness-everything/compare/v0.30.9...v0.30.10) (2026-10-13)
|
|
32
51
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Claude Code live-host replay for issue #306
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-10-13
|
|
4
|
+
|
|
5
|
+
**Host:** Claude Code 2.1.296 on Windows (headless `claude -p`, model haiku)
|
|
6
|
+
|
|
7
|
+
**Plugin source:** `fix/306-verify-cwd-wrapper` worktree loaded with `--plugin-dir` (as `harness-everything@inline`); installed harness-everything plugin disabled via `--settings`. No cached harness-everything plugin appeared in the session's plugin list.
|
|
8
|
+
|
|
9
|
+
**Task workspace:** disposable temporary git project with isolated `HARNESS_STATE_HOME`; no task files were written to this repository.
|
|
10
|
+
|
|
11
|
+
## Observed behavior
|
|
12
|
+
|
|
13
|
+
Runs 1-2 were made without `--include-hook-events`, so only SessionStart hook events appear in their streams. Runs 3-4 were made with `--include-hook-events`, which also exposes PreToolUse, PostToolUse, UserPromptSubmit and Stop hook events.
|
|
14
|
+
|
|
15
|
+
| Check | Run 1 | Run 2 | Run 3 | Run 4 |
|
|
16
|
+
|---|---|---|---|---|
|
|
17
|
+
| Exit code | 0 | 0 | 0 | 0 |
|
|
18
|
+
| Harness plugin loaded | `harness-everything@inline` at worktree | Same | Same | Same |
|
|
19
|
+
| Bash command | `cd "<workspace>" && npm test` | Same | Same | Same |
|
|
20
|
+
| Bash result | `1 test passed`, `is_error: false` | Same | Same | Same |
|
|
21
|
+
| Edit landed (ws-diff) | yes | yes | yes | yes |
|
|
22
|
+
| `lastEditAt` to `lastVerifyAt` | 88 ms, verify later | 89 ms, verify later | 93 ms, verify later | 126 ms, verify later |
|
|
23
|
+
| `lastVerifyAt >= lastEditAt` | true | true | true | true |
|
|
24
|
+
| `lastVerifyExitCode` | null | null | null | null |
|
|
25
|
+
| `[Verification Reminder]` / `verification-after-edit-missing` | not observed (no Stop events in stream) | not observed | absent | absent |
|
|
26
|
+
| Stop `[Workflow Reminder]` output | not observed | not observed | emitted once; 5 unresolved items, no verification item | emitted once; same |
|
|
27
|
+
|
|
28
|
+
Stop observation in runs 3-4: six `Stop` hooks started and responded per run. Five harness Stop commands are registered in `hooks/hooks.json`, and the codex plugin registers one more (`stop-review-gate-hook.mjs`). One Stop response carried output, with exit code 0:
|
|
29
|
+
|
|
30
|
+
- `"hook_name":"Stop","output":"[Workflow Reminder] Unresolved workflow evidence: decompose:pending, compose:pending, requirements:empty, workflow-selection:unconfirmed, execution:not-started. Resolve/verify before claiming completion.\n","exit_code":0,"outcome":"success"`
|
|
31
|
+
|
|
32
|
+
The text matches `workflow-stop-gate.js`. The stream does not name the command for each `hook_id`, so this attribution is by message text. The other five Stop responses were empty with exit code 0. All nine PostToolUse and all PreToolUse responses in runs 3-4 were empty with exit code 0.
|
|
33
|
+
|
|
34
|
+
## Evidence boundary
|
|
35
|
+
|
|
36
|
+
- The `cd <workspace> && npm test` command was requested explicitly in the prompt. It matches the shape observed in the #297 trace, but here it was prompt-directed, not spontaneously generated by the host.
|
|
37
|
+
- One host version (Claude Code 2.1.296) on Windows only.
|
|
38
|
+
- Runs 3 and 4 were launched concurrently, not sequentially as instructed. Each used its own workspace and `HARNESS_STATE_HOME`, and both exited 0. This did not appear to affect results, but it was not the requested procedure.
|
|
39
|
+
- The Stop attribution is by message text, not by a logged hook command. The sixth Stop invocation is unattributed.
|
|
40
|
+
- No `[Verification Reminder]` line appears. The state and code are consistent with that absence (`stop-gate.js` returns early when `lastVerifyAt >= lastEditAt`), but the reminder path was not forced to fire, so it is not live-verified here.
|
|
41
|
+
- `lastVerifyExitCode` is `null` in all runs. The Bash tool result reports only `is_error: false`, so the recorded verify event has no exit code.
|
|
42
|
+
- `workflow-state.json` does not exist; `workflow-run.json` was inspected instead.
|
|
43
|
+
- The raw streams and state files are kept outside the repository because they contain machine-specific paths.
|
|
44
|
+
- This is live-host evidence for the #306 fix. It is not a token or behavior-improvement claim. Mechanism coverage lives in `ci/mechanism-35-verification-contract.test.js` and is a separate evidence layer.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('assert');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
10
|
+
const scripts = [
|
|
11
|
+
path.join(ROOT, 'hooks/scripts/workflow-disposition.js'),
|
|
12
|
+
path.join(ROOT, 'plugins/harness-everything/hooks/scripts/workflow-disposition.js'),
|
|
13
|
+
];
|
|
14
|
+
const stateHome = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-workflow-help-'));
|
|
15
|
+
|
|
16
|
+
function run(script, args) {
|
|
17
|
+
return spawnSync(process.execPath, [script, ...args], {
|
|
18
|
+
cwd: ROOT,
|
|
19
|
+
encoding: 'utf8',
|
|
20
|
+
env: { ...process.env, HARNESS_STATE_HOME: stateHome },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const script of scripts) {
|
|
25
|
+
for (const flag of ['--help', '-h']) {
|
|
26
|
+
const result = run(script, [flag]);
|
|
27
|
+
assert.strictEqual(result.status, 0, `${path.relative(ROOT, script)} ${flag} exits successfully: ${result.stderr}`);
|
|
28
|
+
assert.match(result.stdout, /^Usage: workflow-disposition\.js </);
|
|
29
|
+
assert.strictEqual(result.stderr, '', 'help is written to stdout without an error');
|
|
30
|
+
assert.deepStrictEqual(fs.readdirSync(stateHome), [], 'help does not read or create session state');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const args of [[], ['unknown']]) {
|
|
34
|
+
const result = run(script, args);
|
|
35
|
+
assert.strictEqual(result.status, 2, `${path.relative(ROOT, script)} invalid invocation retains exit 2`);
|
|
36
|
+
assert.match(result.stderr, /Usage: workflow-disposition\.js/);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fs.rmSync(stateHome, { recursive: true, force: true });
|
|
41
|
+
console.log('PASS workflow-disposition help is successful, read-only, and packaged');
|
|
@@ -410,7 +410,192 @@ try {
|
|
|
410
410
|
assert.strictEqual(stop.status, 0);
|
|
411
411
|
assert.doesNotMatch(String(stop.stderr || ''), /Verification Reminder/i);
|
|
412
412
|
|
|
413
|
+
// Issue #306: a host working-directory prelude (`cd <workspace> && <check>`)
|
|
414
|
+
// is recognized only for a validated in-root directory followed by exactly
|
|
415
|
+
// one plain verification command. Canonical and packaged copies must agree.
|
|
416
|
+
fs.rmSync(path.join(classifyRepo, '.harness', 'verify.json'));
|
|
417
|
+
const physicalRepo = fs.realpathSync.native(classifyRepo);
|
|
418
|
+
const posixRepo = physicalRepo.replace(/\\/g, '/');
|
|
419
|
+
const outside = fs.mkdtempSync(path.join(temp, 'outside-'));
|
|
420
|
+
fs.mkdirSync(path.join(classifyRepo, 'pkg'), { recursive: true });
|
|
421
|
+
fs.mkdirSync(path.join(classifyRepo, '--'), { recursive: true });
|
|
422
|
+
fs.mkdirSync(path.join(outside, 'pkg'), { recursive: true });
|
|
423
|
+
fs.writeFileSync(path.join(classifyRepo, 'not-a-dir.txt'), 'x\n');
|
|
424
|
+
const classifiers = {
|
|
425
|
+
canonical: require('../hooks/scripts/lib/workflow-isolation').isVerificationShell,
|
|
426
|
+
packaged: require('../plugins/harness-everything/hooks/scripts/lib/workflow-isolation').isVerificationShell,
|
|
427
|
+
};
|
|
428
|
+
const wrapperCases = (repo) => {
|
|
429
|
+
const positive = [
|
|
430
|
+
`cd "${physicalRepo}" && npm test`,
|
|
431
|
+
`cd '${physicalRepo}' && npm test`,
|
|
432
|
+
`cd '${posixRepo}' && npm test`,
|
|
433
|
+
`cd "${physicalRepo}" && npm test`,
|
|
434
|
+
`cd "${physicalRepo}" && pre-commit run --all-files`,
|
|
435
|
+
`cd ./pkg && npm test`,
|
|
436
|
+
`cd "./pkg" && npm test`,
|
|
437
|
+
`cd . && npm test`,
|
|
438
|
+
'npm test',
|
|
439
|
+
];
|
|
440
|
+
if (!/[^A-Za-z0-9_./:@%+,-]/.test(posixRepo)) positive.push(`cd ${posixRepo} && npm test`);
|
|
441
|
+
if (process.platform === 'win32') {
|
|
442
|
+
positive.push(`cd '/${physicalRepo[0].toLowerCase()}${posixRepo.slice(2)}' && npm test`);
|
|
443
|
+
}
|
|
444
|
+
const negative = [
|
|
445
|
+
`cd "${physicalRepo}" && npm test && rm -rf build`,
|
|
446
|
+
`cd "${physicalRepo}" && npm test || true`,
|
|
447
|
+
'npm test || true',
|
|
448
|
+
'npm test; rm -rf build',
|
|
449
|
+
`cd "${physicalRepo}" && npm test | tee out.txt`,
|
|
450
|
+
`cd "${physicalRepo}" && npm test > out.txt`,
|
|
451
|
+
`cd "${physicalRepo}" && npm test 2>&1`,
|
|
452
|
+
`cd "${physicalRepo}" && npm test $(rm -rf build)`,
|
|
453
|
+
`cd "${physicalRepo}" && npm test \`rm -rf build\``,
|
|
454
|
+
`cd "${physicalRepo}" && npm test &`,
|
|
455
|
+
`cd "${physicalRepo}" && (npm test)`,
|
|
456
|
+
`cd "${physicalRepo}" && npm test\nrm -rf build`,
|
|
457
|
+
`cd "${physicalRepo}" && echo npm test`,
|
|
458
|
+
'echo npm test',
|
|
459
|
+
`cd "${physicalRepo}" && cd "${physicalRepo}" && npm test`,
|
|
460
|
+
`cd "${physicalRepo}\\" && npm test" && rm -rf build`,
|
|
461
|
+
`cd "${physicalRepo};rm -rf build" && npm test`,
|
|
462
|
+
'cd "$HOME" && npm test',
|
|
463
|
+
'cd ~ && npm test',
|
|
464
|
+
`cd "${outside}" && npm test`,
|
|
465
|
+
`cd "${path.join(physicalRepo, '..')}" && npm test`,
|
|
466
|
+
`cd "${path.join(physicalRepo, 'missing')}" && npm test`,
|
|
467
|
+
`cd "${path.join(physicalRepo, 'not-a-dir.txt')}" && npm test`,
|
|
468
|
+
`cd "${physicalRepo}" ; npm test`,
|
|
469
|
+
`cd "${physicalRepo}" || npm test`,
|
|
470
|
+
`pushd "${physicalRepo}" && npm test`,
|
|
471
|
+
`cd "${physicalRepo}" &&`,
|
|
472
|
+
// Bash destination differs from a naive path.resolve: CDPATH-sensitive
|
|
473
|
+
// bare relatives and option-like targets fail closed (PR #307 review).
|
|
474
|
+
'cd pkg && npm test',
|
|
475
|
+
'cd "pkg" && npm test',
|
|
476
|
+
'cd -- && npm test',
|
|
477
|
+
'cd "--" && npm test',
|
|
478
|
+
'cd - && npm test',
|
|
479
|
+
`cd -P "${physicalRepo}" && npm test`,
|
|
480
|
+
];
|
|
481
|
+
if (process.platform === 'win32') negative.push('cd /usr && npm test', 'cd /tmp && npm test');
|
|
482
|
+
return { positive, negative, repo };
|
|
483
|
+
};
|
|
484
|
+
for (const [label, classify] of Object.entries(classifiers)) {
|
|
485
|
+
const { positive, negative } = wrapperCases(classifyRepo);
|
|
486
|
+
for (const command of positive) {
|
|
487
|
+
assert.strictEqual(classify(command, { cwd: classifyRepo, root: classifyRepo }), true, `${label}: wrapper must count: ${command}`);
|
|
488
|
+
}
|
|
489
|
+
for (const command of negative) {
|
|
490
|
+
assert.strictEqual(classify(command, { cwd: classifyRepo, root: classifyRepo }), false, `${label}: must not count: ${JSON.stringify(command)}`);
|
|
491
|
+
}
|
|
492
|
+
// Unexpected inputs fail closed rather than throwing.
|
|
493
|
+
for (const value of [undefined, null, '', ' ', 42, 'cd', 'cd "" && npm test']) {
|
|
494
|
+
assert.strictEqual(classify(value, { cwd: classifyRepo, root: classifyRepo }), false, `${label}: unexpected input ${JSON.stringify(value)}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Where Bash is available, prove the CDPATH hazard is real: `cd pkg` leaves
|
|
499
|
+
// the repository even though <repo>/pkg exists, so the bare form must not count.
|
|
500
|
+
const bashProbe = spawnSync('bash', ['-c', 'cd pkg >/dev/null && pwd'], {
|
|
501
|
+
cwd: classifyRepo,
|
|
502
|
+
encoding: 'utf8',
|
|
503
|
+
// Native path on purpose: MSYS Bash converts it, while a `C:/...` spelling
|
|
504
|
+
// would be split at the drive colon as a CDPATH separator.
|
|
505
|
+
env: { ...process.env, CDPATH: outside },
|
|
506
|
+
});
|
|
507
|
+
if (!bashProbe.error && bashProbe.status === 0) {
|
|
508
|
+
assert.match(bashProbe.stdout.trim(), new RegExp(`${path.basename(outside)}[\\\\/]pkg$`), 'CDPATH redirects a bare cd outside the repository');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// A declared contract still governs the wrapped command: declared checks
|
|
512
|
+
// count through the wrapper only in their declared cwd; undeclared legacy
|
|
513
|
+
// commands and declared commands run elsewhere do not (PR #307 review).
|
|
514
|
+
writeJson(path.join(classifyRepo, '.harness', 'verify.json'), {
|
|
515
|
+
version: 1,
|
|
516
|
+
checks: [
|
|
517
|
+
{ id: 'e2e', run: 'bash scripts/e2e.sh' },
|
|
518
|
+
{ id: 'root-test', run: 'npm test', cwd: '.' },
|
|
519
|
+
{ id: 'pkg-make', run: 'make test', cwd: 'pkg' },
|
|
520
|
+
],
|
|
521
|
+
});
|
|
522
|
+
for (const [label, classify] of Object.entries(classifiers)) {
|
|
523
|
+
const options = { cwd: classifyRepo, root: classifyRepo };
|
|
524
|
+
const pkgOptions = { cwd: path.join(classifyRepo, 'pkg'), root: classifyRepo };
|
|
525
|
+
assert.strictEqual(classify(`cd "${physicalRepo}" && bash scripts/e2e.sh`, options), true, `${label}: declared contract check through wrapper`);
|
|
526
|
+
assert.strictEqual(classify(`cd "${physicalRepo}" && npx jest`, options), false, `${label}: wrapper must not bypass the declared contract`);
|
|
527
|
+
assert.strictEqual(classify(`cd "${physicalRepo}" && bash scripts/e2e.sh && rm -rf build`, options), false, `${label}: chained contract check`);
|
|
528
|
+
assert.strictEqual(classify(`cd "${physicalRepo}" && npm test`, options), true, `${label}: root check in its declared cwd`);
|
|
529
|
+
assert.strictEqual(classify('cd ./pkg && npm test', options), false, `${label}: root check run in a subproject is not root verification`);
|
|
530
|
+
assert.strictEqual(classify(`cd "${path.join(physicalRepo, 'pkg')}" && npm test`, options), false, `${label}: absolute wrong-cwd root check`);
|
|
531
|
+
assert.strictEqual(classify('cd ./pkg && make test', options), true, `${label}: subproject check in its declared cwd`);
|
|
532
|
+
assert.strictEqual(classify(`cd "${physicalRepo}" && make test`, options), false, `${label}: subproject check run at root`);
|
|
533
|
+
assert.strictEqual(classify('npm test', options), true, `${label}: standalone root check at root`);
|
|
534
|
+
assert.strictEqual(classify('npm test', pkgOptions), false, `${label}: standalone root check from the wrong cwd`);
|
|
535
|
+
assert.strictEqual(classify('make test', pkgOptions), true, `${label}: standalone subproject check in its cwd`);
|
|
536
|
+
}
|
|
537
|
+
fs.writeFileSync(path.join(classifyRepo, '.harness', 'verify.json'), '{ broken');
|
|
538
|
+
assert.strictEqual(isVerificationShell(`cd "${physicalRepo}" && npm test`, { cwd: classifyRepo }), false, 'invalid contract still fails closed through the wrapper');
|
|
539
|
+
fs.rmSync(path.join(classifyRepo, '.harness', 'verify.json'));
|
|
540
|
+
|
|
541
|
+
// End-to-end hook replay (Windows-Bash-shaped Claude payload, no exit code):
|
|
542
|
+
// edit -> wrapped verification -> Stop is clean. Repeated twice for determinism.
|
|
543
|
+
const wrappedCommand = `cd "${physicalRepo}" && npm test`;
|
|
544
|
+
for (const scripts of [
|
|
545
|
+
{ label: 'canonical', persist: statePersist, stop: stopGate },
|
|
546
|
+
{ label: 'packaged', persist: path.join(ROOT, 'plugins', 'harness-everything', 'hooks', 'scripts', 'state-persist.js'), stop: path.join(ROOT, 'plugins', 'harness-everything', 'hooks', 'scripts', 'stop-gate.js') },
|
|
547
|
+
]) {
|
|
548
|
+
for (let round = 0; round < 2; round++) {
|
|
549
|
+
const wrapSession = `verify-wrapper-${scripts.label}-${round}`;
|
|
550
|
+
const wrapStateFile = path.join(stateHome, 'workspaces', getWorkspaceKey(classifyRepo), 'state', 'sessions', wrapSession, 'handoff-state.json');
|
|
551
|
+
const readWrapState = () => JSON.parse(fs.readFileSync(wrapStateFile, 'utf8'));
|
|
552
|
+
const bash = (command, extra = {}) => invokeHook(scripts.persist, {
|
|
553
|
+
session_id: wrapSession,
|
|
554
|
+
cwd: classifyRepo,
|
|
555
|
+
hook_event_name: 'PostToolUse',
|
|
556
|
+
tool_name: 'Bash',
|
|
557
|
+
tool_input: { command },
|
|
558
|
+
tool_response: { stdout: 'ok', stderr: '' },
|
|
559
|
+
...extra,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
assert.strictEqual(invokeHook(scripts.persist, {
|
|
563
|
+
session_id: wrapSession,
|
|
564
|
+
cwd: classifyRepo,
|
|
565
|
+
hook_event_name: 'PostToolUse',
|
|
566
|
+
tool_name: 'Edit',
|
|
567
|
+
tool_input: { file_path: path.join(classifyRepo, 'README.md') },
|
|
568
|
+
tool_response: { stdout: '' },
|
|
569
|
+
}).status, 0);
|
|
570
|
+
const editedAt = readWrapState().lastEditAt;
|
|
571
|
+
assert.ok(Number.isFinite(editedAt), `${scripts.label}: edit recorded`);
|
|
572
|
+
|
|
573
|
+
// Failed or unconfirmed outcomes never mark a passing verification.
|
|
574
|
+
assert.strictEqual(bash(wrappedCommand, { hook_event_name: 'PostToolUseFailure', tool_response: { stderr: 'boom' } }).status, 0);
|
|
575
|
+
assert.ok(!readWrapState().lastVerifyAt, `${scripts.label}: PostToolUseFailure must not verify`);
|
|
576
|
+
assert.strictEqual(bash(wrappedCommand, { tool_response: { stdout: '', stderr: 'boom', exitCode: 1 } }).status, 0);
|
|
577
|
+
assert.ok(!readWrapState().lastVerifyAt, `${scripts.label}: nonzero exit must not verify`);
|
|
578
|
+
assert.strictEqual(bash(wrappedCommand, { hook_event_name: undefined }).status, 0);
|
|
579
|
+
assert.ok(!readWrapState().lastVerifyAt, `${scripts.label}: unknown event without exit code must not verify`);
|
|
580
|
+
assert.strictEqual(bash(`${wrappedCommand} && rm -rf build`).status, 0);
|
|
581
|
+
assert.ok(!readWrapState().lastVerifyAt, `${scripts.label}: chained wrapper must not verify`);
|
|
582
|
+
|
|
583
|
+
const failingStop = invokeHook(scripts.stop, { session_id: wrapSession, cwd: classifyRepo });
|
|
584
|
+
assert.strictEqual(failingStop.status, 0, `${scripts.label}: Stop stays fail-open`);
|
|
585
|
+
assert.match(String(failingStop.stderr || ''), /Verification Reminder/i, `${scripts.label}: reminder before verification`);
|
|
586
|
+
|
|
587
|
+
assert.strictEqual(bash(wrappedCommand).status, 0);
|
|
588
|
+
const verified = readWrapState();
|
|
589
|
+
assert.ok(verified.lastVerifyAt >= verified.lastEditAt, `${scripts.label}: wrapper verification recorded ${JSON.stringify(verified)}`);
|
|
590
|
+
assert.strictEqual(verified.lastVerifyExitCode, null);
|
|
591
|
+
const cleanStop = invokeHook(scripts.stop, { session_id: wrapSession, cwd: classifyRepo });
|
|
592
|
+
assert.strictEqual(cleanStop.status, 0);
|
|
593
|
+
assert.doesNotMatch(String(cleanStop.stderr || ''), /Verification Reminder/i, `${scripts.label}: clean Stop after wrapped verification`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
413
597
|
console.log('Issue #242 verification contract: taxonomy, discovery, execution safety, multi-root scope, hook recognition, and Stop evidence verified.');
|
|
598
|
+
console.log('Issue #306 host cwd wrapper: validated prelude recognition, negative chaining/injection/out-of-root cases, contract governance, canonical/packaged parity, and hook-to-Stop replay verified.');
|
|
414
599
|
} finally {
|
|
415
600
|
fs.rmSync(temp, { recursive: true, force: true });
|
|
416
601
|
}
|
|
@@ -43,7 +43,8 @@ function processState(payload) {
|
|
|
43
43
|
if (isShell) {
|
|
44
44
|
const commandClass = classifyShell(command);
|
|
45
45
|
if (commandClass !== 'read-only' && commandClass !== 'worktree-setup') state.lastEditAt = Date.now();
|
|
46
|
-
|
|
46
|
+
// Verification evidence needs a confirmed success: a PostToolUse event or an explicit zero exit.
|
|
47
|
+
if (!isFailed && (isSuccessEvent || exitCode === 0) && isVerificationShell(command, { cwd: cwdOf(payload, root), root })) { state.lastVerifyAt = Date.now(); state.lastVerifyExitCode = exitCode ?? null; }
|
|
47
48
|
}
|
|
48
49
|
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
|
|
49
50
|
observeTool(payload);
|
|
@@ -149,6 +149,10 @@ function activatePlan(workflow, plan) {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
function main() {
|
|
152
|
+
if (process.argv[2] === '--help' || process.argv[2] === '-h') {
|
|
153
|
+
process.stdout.write(usage() + '\n');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
152
156
|
const args = parseArgs(process.argv.slice(2));
|
|
153
157
|
if (!WORKFLOW_CONTROLLER_COMMANDS.has(args.command)) throw new Error(usage());
|
|
154
158
|
const sessionId = args.sessionId || readCurrentSession(getWorkspaceRoot());
|
package/package.json
CHANGED
|
@@ -43,7 +43,8 @@ function processState(payload) {
|
|
|
43
43
|
if (isShell) {
|
|
44
44
|
const commandClass = classifyShell(command);
|
|
45
45
|
if (commandClass !== 'read-only' && commandClass !== 'worktree-setup') state.lastEditAt = Date.now();
|
|
46
|
-
|
|
46
|
+
// Verification evidence needs a confirmed success: a PostToolUse event or an explicit zero exit.
|
|
47
|
+
if (!isFailed && (isSuccessEvent || exitCode === 0) && isVerificationShell(command, { cwd: cwdOf(payload, root), root })) { state.lastVerifyAt = Date.now(); state.lastVerifyExitCode = exitCode ?? null; }
|
|
47
48
|
}
|
|
48
49
|
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
|
|
49
50
|
observeTool(payload);
|
|
@@ -149,6 +149,10 @@ function activatePlan(workflow, plan) {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
function main() {
|
|
152
|
+
if (process.argv[2] === '--help' || process.argv[2] === '-h') {
|
|
153
|
+
process.stdout.write(usage() + '\n');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
152
156
|
const args = parseArgs(process.argv.slice(2));
|
|
153
157
|
if (!WORKFLOW_CONTROLLER_COMMANDS.has(args.command)) throw new Error(usage());
|
|
154
158
|
const sessionId = args.sessionId || readCurrentSession(getWorkspaceRoot());
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "harness-everything",
|
|
4
|
-
"version": "0.31.
|
|
4
|
+
"version": "0.31.2",
|
|
5
5
|
"description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Miya Daniel",
|
|
@@ -303,6 +303,7 @@ function verificationCommandSet(cwd, options = {}) {
|
|
|
303
303
|
contract,
|
|
304
304
|
contractInvalid: false,
|
|
305
305
|
commands: contract.checks.map(check => check.run),
|
|
306
|
+
checks: contract.checks.map(check => ({ run: check.run, cwd: check.cwd })),
|
|
306
307
|
commitHookDeclared: contract.checks.some(check => /^pre-commit\s+/i.test(normalizeCommand(check.run))),
|
|
307
308
|
};
|
|
308
309
|
}
|
|
@@ -323,7 +324,75 @@ function verificationCommandSet(cwd, options = {}) {
|
|
|
323
324
|
};
|
|
324
325
|
}
|
|
325
326
|
|
|
327
|
+
// Hosts such as Claude Code on Windows Bash may prefix a shell call with a
|
|
328
|
+
// working-directory prelude: `cd <workspace> && <command>`. Recognize exactly
|
|
329
|
+
// one such prelude lexically (never evaluating shell text) and only when the
|
|
330
|
+
// target is an existing directory inside the repository root. Only spellings
|
|
331
|
+
// whose Bash destination is stable are accepted: absolute paths or explicit
|
|
332
|
+
// ./ and ../ relatives. Bare relatives are CDPATH-sensitive, and option-like
|
|
333
|
+
// targets (-, --, -P ...) change cd semantics, so both fail closed. The remaining
|
|
334
|
+
// command must be one plain command: no further control operators,
|
|
335
|
+
// redirection, substitution, expansion, subshell, or background syntax.
|
|
336
|
+
const CWD_PRELUDE_RE = /^cd\s+(?:"([^"]*)"|'([^']*)'|([A-Za-z0-9_./:@%+,-]+))\s*&&\s*([\s\S]+)$/;
|
|
337
|
+
const UNSAFE_WRAPPED_RE = /[`$<>&|;()\r\n]/;
|
|
338
|
+
const UNSAFE_DOUBLE_QUOTED_RE = /[$`!]|\\[\\"$`]|\\$/;
|
|
339
|
+
|
|
340
|
+
function hostPathToNative(value, platform = process.platform) {
|
|
341
|
+
if (platform !== 'win32') return value;
|
|
342
|
+
const drive = value.match(/^\/([A-Za-z])(?:\/([\s\S]*))?$/);
|
|
343
|
+
return drive ? `${drive[1].toUpperCase()}:\\${(drive[2] || '').replace(/\//g, '\\')}` : value;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function stableCdTarget(target, platform = process.platform) {
|
|
347
|
+
if (target.startsWith('-')) return false;
|
|
348
|
+
if (/^\.\.?(?:\/|$)/.test(target)) return true;
|
|
349
|
+
if (platform === 'win32') {
|
|
350
|
+
if (/^\.\.?\\/.test(target) || /^[A-Za-z]:[\\/]/.test(target)) return true;
|
|
351
|
+
// Git Bash maps only /<drive>/... onto Windows paths; other absolute
|
|
352
|
+
// POSIX paths (/tmp, /usr) are MSYS mounts that Node cannot resolve alike.
|
|
353
|
+
return /^\/[A-Za-z](?:\/|$)/.test(target);
|
|
354
|
+
}
|
|
355
|
+
return target.startsWith('/');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function parseCwdPrelude(command, options = {}) {
|
|
359
|
+
const match = String(command || '').trim().match(CWD_PRELUDE_RE);
|
|
360
|
+
if (!match) return null;
|
|
361
|
+
const [, doubleQuoted, singleQuoted, bare, restRaw] = match;
|
|
362
|
+
const target = doubleQuoted ?? singleQuoted ?? bare;
|
|
363
|
+
const rest = restRaw.trim();
|
|
364
|
+
if (!target || !rest) return null;
|
|
365
|
+
if (doubleQuoted !== undefined && UNSAFE_DOUBLE_QUOTED_RE.test(target)) return null;
|
|
366
|
+
if (UNSAFE_WRAPPED_RE.test(rest) || hasControlOperators(rest)) return null;
|
|
367
|
+
if (!stableCdTarget(target, options.platform)) return null;
|
|
368
|
+
|
|
369
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
370
|
+
let resolved;
|
|
371
|
+
let root;
|
|
372
|
+
try {
|
|
373
|
+
resolved = fs.realpathSync.native(path.resolve(cwd, hostPathToNative(target, options.platform)));
|
|
374
|
+
if (!fs.statSync(resolved).isDirectory()) return null;
|
|
375
|
+
root = fs.realpathSync.native(path.resolve(options.root || findGitRoot(cwd)));
|
|
376
|
+
} catch (_) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
if (!within(root, resolved)) return null;
|
|
380
|
+
return { cwd: resolved, command: rest };
|
|
381
|
+
}
|
|
382
|
+
|
|
326
383
|
function isVerificationCommand(command, options = {}) {
|
|
384
|
+
const wrapped = parseCwdPrelude(command, options);
|
|
385
|
+
if (wrapped) return isPlainVerificationCommand(wrapped.command, { ...options, cwd: wrapped.cwd });
|
|
386
|
+
return isPlainVerificationCommand(command, options);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function cwdKey(dir) {
|
|
390
|
+
let resolved = path.resolve(dir);
|
|
391
|
+
try { resolved = fs.realpathSync.native(resolved); } catch (_) {}
|
|
392
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function isPlainVerificationCommand(command, options = {}) {
|
|
327
396
|
const cwd = path.resolve(options.cwd || process.cwd());
|
|
328
397
|
const actual = normalizeCommand(command);
|
|
329
398
|
if (!actual || hasControlOperators(actual)) return false;
|
|
@@ -333,8 +402,15 @@ function isVerificationCommand(command, options = {}) {
|
|
|
333
402
|
if (isGitCommit(actual)) {
|
|
334
403
|
return !!set.commitHookDeclared && !bypassesHooks(actual) && hasCommitHook(set.repoRoot);
|
|
335
404
|
}
|
|
405
|
+
// A declared contract check counts only when it ran in its declared cwd;
|
|
406
|
+
// otherwise a matching command in another directory (e.g. a subproject)
|
|
407
|
+
// would be credited as the root check.
|
|
408
|
+
if (set.contract) {
|
|
409
|
+
const executed = cwdKey(cwd);
|
|
410
|
+
return set.checks.some(check => commandMatches(actual, check.run) && cwdKey(check.cwd) === executed);
|
|
411
|
+
}
|
|
336
412
|
if (set.commands.some(expected => commandMatches(actual, expected))) return true;
|
|
337
|
-
return
|
|
413
|
+
return legacyVerificationCommand(actual);
|
|
338
414
|
}
|
|
339
415
|
|
|
340
416
|
module.exports = {
|
|
@@ -350,6 +426,7 @@ module.exports = {
|
|
|
350
426
|
isVerificationCommand,
|
|
351
427
|
legacyVerificationCommand,
|
|
352
428
|
normalizeCommand,
|
|
429
|
+
parseCwdPrelude,
|
|
353
430
|
readContract,
|
|
354
431
|
resolveContained,
|
|
355
432
|
verificationCommandSet,
|
package/sbom.cdx.json
CHANGED
|
@@ -2,23 +2,23 @@
|
|
|
2
2
|
"$schema": "https://cyclonedx.org/schema/bom-1.7.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.7",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:9d3ce484-1be2-5a01-aaf1-830f69f479f8",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-10-
|
|
8
|
+
"timestamp": "2026-10-15T15:15:14.070Z",
|
|
9
9
|
"tools": [
|
|
10
10
|
{
|
|
11
11
|
"vendor": "Harness Everything",
|
|
12
12
|
"name": "generate-sbom.js",
|
|
13
|
-
"version": "0.31.
|
|
13
|
+
"version": "0.31.2"
|
|
14
14
|
}
|
|
15
15
|
],
|
|
16
16
|
"component": {
|
|
17
17
|
"type": "application",
|
|
18
|
-
"bom-ref": "pkg:npm/harness-everything@0.31.
|
|
18
|
+
"bom-ref": "pkg:npm/harness-everything@0.31.2",
|
|
19
19
|
"name": "harness-everything",
|
|
20
|
-
"version": "0.31.
|
|
21
|
-
"purl": "pkg:npm/harness-everything@0.31.
|
|
20
|
+
"version": "0.31.2",
|
|
21
|
+
"purl": "pkg:npm/harness-everything@0.31.2"
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"components": [
|
|
@@ -920,15 +920,15 @@
|
|
|
920
920
|
},
|
|
921
921
|
{
|
|
922
922
|
"type": "library",
|
|
923
|
-
"bom-ref": "pkg:npm/fast-uri@3.1.
|
|
923
|
+
"bom-ref": "pkg:npm/fast-uri@3.1.8",
|
|
924
924
|
"name": "fast-uri",
|
|
925
|
-
"version": "3.1.
|
|
925
|
+
"version": "3.1.8",
|
|
926
926
|
"scope": "excluded",
|
|
927
|
-
"purl": "pkg:npm/fast-uri@3.1.
|
|
927
|
+
"purl": "pkg:npm/fast-uri@3.1.8",
|
|
928
928
|
"hashes": [
|
|
929
929
|
{
|
|
930
930
|
"alg": "SHA-512",
|
|
931
|
-
"content": "
|
|
931
|
+
"content": "GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg=="
|
|
932
932
|
}
|
|
933
933
|
]
|
|
934
934
|
},
|
|
@@ -1896,7 +1896,7 @@
|
|
|
1896
1896
|
"ref": "pkg:npm/ajv@8.20.0",
|
|
1897
1897
|
"dependsOn": [
|
|
1898
1898
|
"pkg:npm/fast-deep-equal@3.1.3",
|
|
1899
|
-
"pkg:npm/fast-uri@3.1.
|
|
1899
|
+
"pkg:npm/fast-uri@3.1.8",
|
|
1900
1900
|
"pkg:npm/json-schema-traverse@1.0.0",
|
|
1901
1901
|
"pkg:npm/require-from-string@2.0.2"
|
|
1902
1902
|
]
|
|
@@ -1999,7 +1999,7 @@
|
|
|
1999
1999
|
]
|
|
2000
2000
|
},
|
|
2001
2001
|
{
|
|
2002
|
-
"ref": "pkg:npm/harness-everything@0.31.
|
|
2002
|
+
"ref": "pkg:npm/harness-everything@0.31.2",
|
|
2003
2003
|
"dependsOn": [
|
|
2004
2004
|
"pkg:npm/%40commitlint/cli@19.8.1",
|
|
2005
2005
|
"pkg:npm/%40commitlint/config-conventional@19.8.1",
|
|
@@ -303,6 +303,7 @@ function verificationCommandSet(cwd, options = {}) {
|
|
|
303
303
|
contract,
|
|
304
304
|
contractInvalid: false,
|
|
305
305
|
commands: contract.checks.map(check => check.run),
|
|
306
|
+
checks: contract.checks.map(check => ({ run: check.run, cwd: check.cwd })),
|
|
306
307
|
commitHookDeclared: contract.checks.some(check => /^pre-commit\s+/i.test(normalizeCommand(check.run))),
|
|
307
308
|
};
|
|
308
309
|
}
|
|
@@ -323,7 +324,75 @@ function verificationCommandSet(cwd, options = {}) {
|
|
|
323
324
|
};
|
|
324
325
|
}
|
|
325
326
|
|
|
327
|
+
// Hosts such as Claude Code on Windows Bash may prefix a shell call with a
|
|
328
|
+
// working-directory prelude: `cd <workspace> && <command>`. Recognize exactly
|
|
329
|
+
// one such prelude lexically (never evaluating shell text) and only when the
|
|
330
|
+
// target is an existing directory inside the repository root. Only spellings
|
|
331
|
+
// whose Bash destination is stable are accepted: absolute paths or explicit
|
|
332
|
+
// ./ and ../ relatives. Bare relatives are CDPATH-sensitive, and option-like
|
|
333
|
+
// targets (-, --, -P ...) change cd semantics, so both fail closed. The remaining
|
|
334
|
+
// command must be one plain command: no further control operators,
|
|
335
|
+
// redirection, substitution, expansion, subshell, or background syntax.
|
|
336
|
+
const CWD_PRELUDE_RE = /^cd\s+(?:"([^"]*)"|'([^']*)'|([A-Za-z0-9_./:@%+,-]+))\s*&&\s*([\s\S]+)$/;
|
|
337
|
+
const UNSAFE_WRAPPED_RE = /[`$<>&|;()\r\n]/;
|
|
338
|
+
const UNSAFE_DOUBLE_QUOTED_RE = /[$`!]|\\[\\"$`]|\\$/;
|
|
339
|
+
|
|
340
|
+
function hostPathToNative(value, platform = process.platform) {
|
|
341
|
+
if (platform !== 'win32') return value;
|
|
342
|
+
const drive = value.match(/^\/([A-Za-z])(?:\/([\s\S]*))?$/);
|
|
343
|
+
return drive ? `${drive[1].toUpperCase()}:\\${(drive[2] || '').replace(/\//g, '\\')}` : value;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function stableCdTarget(target, platform = process.platform) {
|
|
347
|
+
if (target.startsWith('-')) return false;
|
|
348
|
+
if (/^\.\.?(?:\/|$)/.test(target)) return true;
|
|
349
|
+
if (platform === 'win32') {
|
|
350
|
+
if (/^\.\.?\\/.test(target) || /^[A-Za-z]:[\\/]/.test(target)) return true;
|
|
351
|
+
// Git Bash maps only /<drive>/... onto Windows paths; other absolute
|
|
352
|
+
// POSIX paths (/tmp, /usr) are MSYS mounts that Node cannot resolve alike.
|
|
353
|
+
return /^\/[A-Za-z](?:\/|$)/.test(target);
|
|
354
|
+
}
|
|
355
|
+
return target.startsWith('/');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function parseCwdPrelude(command, options = {}) {
|
|
359
|
+
const match = String(command || '').trim().match(CWD_PRELUDE_RE);
|
|
360
|
+
if (!match) return null;
|
|
361
|
+
const [, doubleQuoted, singleQuoted, bare, restRaw] = match;
|
|
362
|
+
const target = doubleQuoted ?? singleQuoted ?? bare;
|
|
363
|
+
const rest = restRaw.trim();
|
|
364
|
+
if (!target || !rest) return null;
|
|
365
|
+
if (doubleQuoted !== undefined && UNSAFE_DOUBLE_QUOTED_RE.test(target)) return null;
|
|
366
|
+
if (UNSAFE_WRAPPED_RE.test(rest) || hasControlOperators(rest)) return null;
|
|
367
|
+
if (!stableCdTarget(target, options.platform)) return null;
|
|
368
|
+
|
|
369
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
370
|
+
let resolved;
|
|
371
|
+
let root;
|
|
372
|
+
try {
|
|
373
|
+
resolved = fs.realpathSync.native(path.resolve(cwd, hostPathToNative(target, options.platform)));
|
|
374
|
+
if (!fs.statSync(resolved).isDirectory()) return null;
|
|
375
|
+
root = fs.realpathSync.native(path.resolve(options.root || findGitRoot(cwd)));
|
|
376
|
+
} catch (_) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
if (!within(root, resolved)) return null;
|
|
380
|
+
return { cwd: resolved, command: rest };
|
|
381
|
+
}
|
|
382
|
+
|
|
326
383
|
function isVerificationCommand(command, options = {}) {
|
|
384
|
+
const wrapped = parseCwdPrelude(command, options);
|
|
385
|
+
if (wrapped) return isPlainVerificationCommand(wrapped.command, { ...options, cwd: wrapped.cwd });
|
|
386
|
+
return isPlainVerificationCommand(command, options);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function cwdKey(dir) {
|
|
390
|
+
let resolved = path.resolve(dir);
|
|
391
|
+
try { resolved = fs.realpathSync.native(resolved); } catch (_) {}
|
|
392
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function isPlainVerificationCommand(command, options = {}) {
|
|
327
396
|
const cwd = path.resolve(options.cwd || process.cwd());
|
|
328
397
|
const actual = normalizeCommand(command);
|
|
329
398
|
if (!actual || hasControlOperators(actual)) return false;
|
|
@@ -333,8 +402,15 @@ function isVerificationCommand(command, options = {}) {
|
|
|
333
402
|
if (isGitCommit(actual)) {
|
|
334
403
|
return !!set.commitHookDeclared && !bypassesHooks(actual) && hasCommitHook(set.repoRoot);
|
|
335
404
|
}
|
|
405
|
+
// A declared contract check counts only when it ran in its declared cwd;
|
|
406
|
+
// otherwise a matching command in another directory (e.g. a subproject)
|
|
407
|
+
// would be credited as the root check.
|
|
408
|
+
if (set.contract) {
|
|
409
|
+
const executed = cwdKey(cwd);
|
|
410
|
+
return set.checks.some(check => commandMatches(actual, check.run) && cwdKey(check.cwd) === executed);
|
|
411
|
+
}
|
|
336
412
|
if (set.commands.some(expected => commandMatches(actual, expected))) return true;
|
|
337
|
-
return
|
|
413
|
+
return legacyVerificationCommand(actual);
|
|
338
414
|
}
|
|
339
415
|
|
|
340
416
|
module.exports = {
|
|
@@ -350,6 +426,7 @@ module.exports = {
|
|
|
350
426
|
isVerificationCommand,
|
|
351
427
|
legacyVerificationCommand,
|
|
352
428
|
normalizeCommand,
|
|
429
|
+
parseCwdPrelude,
|
|
353
430
|
readContract,
|
|
354
431
|
resolveContained,
|
|
355
432
|
verificationCommandSet,
|