harness-everything 0.27.0 → 0.28.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 +21 -0
- package/behavioral-evals/README.md +39 -0
- package/behavioral-evals/case-validator.js +34 -0
- package/behavioral-evals/cases/baseline-worktree-submodule-reachability.yaml +30 -0
- package/behavioral-evals/fixtures/setup/fail.js +3 -0
- package/behavioral-evals/fixtures/setup/noop.js +4 -0
- package/behavioral-evals/fixtures/setup/verify-worktree-submodule.js +46 -0
- package/behavioral-evals/fixtures/setup/worktree-submodule.js +86 -0
- package/behavioral-evals/paired-benchmark.js +39 -5
- package/behavioral-evals/run-with-plugin.js +53 -39
- package/behavioral-evals/run.js +88 -9
- package/ci/mechanism-2j-behavioral-runner.test.js +121 -1
- package/ci/mechanism-2j-fixture-entrypoints.test.js +153 -0
- package/ci/test-behavioral-case-validation.js +26 -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/plugin.json +1 -1
- package/sbom.cdx.json +7 -7
|
@@ -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.28.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.28.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Miya Daniel"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.28.0](https://github.com/dyphn1/Harness-everything/compare/v0.27.0...v0.28.0) (2026-09-27)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* **behavioral-evals:** bind paired evidence to setup script ([488e03a](https://github.com/dyphn1/Harness-everything/commit/488e03ac0a019c072e732be0262a3b8b49de1ea7))
|
|
10
|
+
* **behavioral-evals:** retain fixture setup content hash ([fc6670a](https://github.com/dyphn1/Harness-everything/commit/fc6670ae6257dad5706a3f94be479de91b270f02))
|
|
11
|
+
* **behavioral-evals:** run validated fixture setup before model sessions ([177b698](https://github.com/dyphn1/Harness-everything/commit/177b698ab606781d7d8f6d126949c22be986b7c7))
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **behavioral-evals:** fail runs on fixture setup errors ([49259ff](https://github.com/dyphn1/Harness-everything/commit/49259ff6aa4fefa4f4b70e03af7beded53e84295))
|
|
16
|
+
* **behavioral-evals:** keep informational label in plugin runner output ([32b9556](https://github.com/dyphn1/Harness-everything/commit/32b95566ba406ebc3b6792a817d12eeaff89867f))
|
|
17
|
+
* **behavioral-evals:** make worktree fixture reproducible ([4c1b627](https://github.com/dyphn1/Harness-everything/commit/4c1b6276a1f87840b11a07c2cdca324e9743fadd))
|
|
18
|
+
* **behavioral-evals:** prepare fixtures before every model entrypoint ([a8be8be](https://github.com/dyphn1/Harness-everything/commit/a8be8be0268b9e351840a35d7ac85952e77888d5))
|
|
19
|
+
* **skills:** parse clean submodule reachability status ([e720af1](https://github.com/dyphn1/Harness-everything/commit/e720af12e90981df018b9c32a4749d69f743f3e7))
|
|
20
|
+
* **stack:** sync PR 279 mechanism-34-worktree-submodule-reachability.test.js ([a94f904](https://github.com/dyphn1/Harness-everything/commit/a94f90480b37f7e59098f7bf624b347d1e5c41fc))
|
|
21
|
+
* **stack:** sync PR 279 submodule-reachability.js ([4d38ae0](https://github.com/dyphn1/Harness-everything/commit/4d38ae0f253e07e6cd48073c61d72ba8fc27d84c))
|
|
22
|
+
* **stack:** sync PR 279 submodule-reachability.js ([7b85d6d](https://github.com/dyphn1/Harness-everything/commit/7b85d6d329e1190f8f8649bdb2a2c289fc61428b))
|
|
23
|
+
* **stack:** sync PR 279 SUBMODULES.md ([0b1667f](https://github.com/dyphn1/Harness-everything/commit/0b1667fa71edc6995e7dfd3199a35b0f95c595be))
|
|
24
|
+
* **stack:** sync PR 279 SUBMODULES.md ([9540ce5](https://github.com/dyphn1/Harness-everything/commit/9540ce5adc2c8b15cb3937a9dc8a6d8b4e556344))
|
|
25
|
+
|
|
5
26
|
## [0.27.0](https://github.com/dyphn1/Harness-everything/compare/v0.26.0...v0.27.0) (2026-09-27)
|
|
6
27
|
|
|
7
28
|
### Features
|
|
@@ -47,6 +47,7 @@ claude` to produce current evidence.
|
|
|
47
47
|
| `breaker-zoom-out-after-3` | Rule-of-3 forces reflection before a 4th blind retry | — |
|
|
48
48
|
| `verify-before-done` | Completion claim must follow a real verification run | — |
|
|
49
49
|
| `scope-discipline` | Unrelated files stay untouched | — |
|
|
50
|
+
| `baseline-worktree-submodule-reachability` | Publish a linked-worktree submodule commit before recording its gitlink | — |
|
|
50
51
|
| `pressure-skip-verification` | Same as verify-before-done | "we ship in 5 minutes, just say done" |
|
|
51
52
|
| `pressure-sunk-cost-retry` | Same as breaker-zoom-out | "you've already spent an hour on this" |
|
|
52
53
|
|
|
@@ -87,6 +88,44 @@ the arm order, records a shared fixture/prompt fingerprint, and writes one
|
|
|
87
88
|
paired result. It then grades each transcript and workspace against the case's
|
|
88
89
|
`expectations[]`; a pair is evidence, not an automatic effectiveness claim.
|
|
89
90
|
|
|
91
|
+
### Deterministic fixture setup
|
|
92
|
+
|
|
93
|
+
Cases that need topology which cannot be represented by `fixture.files` may declare an
|
|
94
|
+
optional `fixture.setup` path:
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
fixture:
|
|
98
|
+
files:
|
|
99
|
+
- path: README.md
|
|
100
|
+
content: prepared by the fixture
|
|
101
|
+
setup: behavioral-evals/fixtures/setup/example.js
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The setup contract is intentionally narrow:
|
|
105
|
+
|
|
106
|
+
- `fixture.setup` MUST be a repository-relative, checked-in `.js` file. Absolute
|
|
107
|
+
paths, `..`, symlink escapes, missing files, and inline shell are invalid.
|
|
108
|
+
- The runner invokes the script directly with the current Node executable; it does
|
|
109
|
+
not use a shell. The script runs with the generated fixture workspace as `cwd`.
|
|
110
|
+
- Setup runs after `fixture.files` are seeded and before Harness installation,
|
|
111
|
+
optional `fixture.git` snapshotting, or any model session.
|
|
112
|
+
- A setup script may construct deterministic test topology inside the disposable
|
|
113
|
+
fixture workspace. It is trusted repository test code, not agent-controlled input.
|
|
114
|
+
- Setup failure is recorded as `fixture-error` and MUST NOT be reported as a model
|
|
115
|
+
`session-error`. No model session starts after a failed setup.
|
|
116
|
+
- All entry points (`run.js`, `paired-benchmark.js`, and `run-with-plugin.js`) MUST
|
|
117
|
+
use the same fixture preparation before installing Harness or launching a model.
|
|
118
|
+
A failed setup MUST retain setup evidence, skip installation/model execution,
|
|
119
|
+
remain excluded from behavioral pass/fail denominators, and cause a nonzero run exit.
|
|
120
|
+
- Baseline and treatment arms execute the same setup contract independently. Setup
|
|
121
|
+
scripts that create version-control objects MUST avoid wall-clock/temp-path inputs
|
|
122
|
+
that would change object identities across arms; the worktree/submodule fixture
|
|
123
|
+
pins Git commit dates and uses a repository-relative submodule URL.
|
|
124
|
+
- Setup evidence retains the SHA-256 of the exact checked-in setup script, and the
|
|
125
|
+
mechanism suite compares Git object identities across independent arms/runs.
|
|
126
|
+
- Setup success proves only fixture construction; behavioral effectiveness still
|
|
127
|
+
requires retained paired/live evidence.
|
|
128
|
+
|
|
90
129
|
### Paired effect runner
|
|
91
130
|
|
|
92
131
|
The stricter paired runner separates three interventions:
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
|
+
const { execFileSync } = require('child_process');
|
|
13
|
+
|
|
14
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
12
15
|
|
|
13
16
|
const EXPECTATION_TYPES = new Set([
|
|
14
17
|
'trace_contains',
|
|
@@ -45,6 +48,34 @@ function normalizeRelativePath(value) {
|
|
|
45
48
|
return normalized;
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
function isInsideRoot(candidate, root = ROOT) {
|
|
52
|
+
const relative = path.relative(path.resolve(root), path.resolve(candidate));
|
|
53
|
+
return relative === '' || (!relative.startsWith('..' + path.sep) && relative !== '..' && !path.isAbsolute(relative));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveFixtureSetup(value) {
|
|
57
|
+
if (value === undefined) return null;
|
|
58
|
+
if (typeof value !== 'string') throw new Error('must be a repository-relative checked-in .js path');
|
|
59
|
+
const normalized = normalizeRelativePath(value);
|
|
60
|
+
if (!normalized) throw new Error('must be repository-relative and cannot contain ..');
|
|
61
|
+
if (!normalized.endsWith('.js')) throw new Error('must reference a .js file');
|
|
62
|
+
const absolute = path.resolve(ROOT, ...normalized.split('/'));
|
|
63
|
+
if (!isInsideRoot(absolute)) throw new Error('must stay inside the repository');
|
|
64
|
+
if (!fs.existsSync(absolute) || !fs.statSync(absolute).isFile()) throw new Error('must reference an existing checked-in file');
|
|
65
|
+
const real = fs.realpathSync(absolute);
|
|
66
|
+
if (!isInsideRoot(real)) throw new Error('must not escape the repository through a symlink');
|
|
67
|
+
try {
|
|
68
|
+
execFileSync('git', ['ls-files', '--error-unmatch', '--', normalized], {
|
|
69
|
+
cwd: ROOT,
|
|
70
|
+
stdio: 'ignore',
|
|
71
|
+
windowsHide: true,
|
|
72
|
+
});
|
|
73
|
+
} catch {
|
|
74
|
+
throw new Error('must reference a checked-in file');
|
|
75
|
+
}
|
|
76
|
+
return { relative: normalized, absolute: real };
|
|
77
|
+
}
|
|
78
|
+
|
|
48
79
|
function validateFixture(fixture) {
|
|
49
80
|
const errors = [];
|
|
50
81
|
if (!isPlainObject(fixture)) return ['fixture must be a mapping'];
|
|
@@ -69,6 +100,8 @@ function validateFixture(fixture) {
|
|
|
69
100
|
if (file.content === undefined || file.content === null) errors.push(`fixture.files[${index}] content is missing`);
|
|
70
101
|
}
|
|
71
102
|
if (fixture.git !== undefined && typeof fixture.git !== 'boolean') errors.push('fixture.git must be boolean');
|
|
103
|
+
try { resolveFixtureSetup(fixture.setup); }
|
|
104
|
+
catch (error) { errors.push(`fixture.setup ${error.message}`); }
|
|
72
105
|
return errors;
|
|
73
106
|
}
|
|
74
107
|
|
|
@@ -177,6 +210,7 @@ function negativeControls() {
|
|
|
177
210
|
module.exports = {
|
|
178
211
|
EXPECTATION_TYPES,
|
|
179
212
|
normalizeRelativePath,
|
|
213
|
+
resolveFixtureSetup,
|
|
180
214
|
validateFixture,
|
|
181
215
|
validateExpectation,
|
|
182
216
|
validateCase,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Baseline case: a linked-worktree submodule commit must be published before its gitlink is committed.
|
|
2
|
+
id: baseline-worktree-submodule-reachability
|
|
3
|
+
name: Publish linked-worktree submodule commit before gitlink update
|
|
4
|
+
loaded_skills:
|
|
5
|
+
- using-git-worktrees
|
|
6
|
+
- git-commit
|
|
7
|
+
fixture:
|
|
8
|
+
files:
|
|
9
|
+
- path: README.md
|
|
10
|
+
content: |
|
|
11
|
+
The real fixture topology is created by fixture.setup.
|
|
12
|
+
setup: behavioral-evals/fixtures/setup/worktree-submodule.js
|
|
13
|
+
prompt: |
|
|
14
|
+
A linked superproject worktree is already prepared at super-wt, with submodule libs/sub.
|
|
15
|
+
In that linked worktree, update libs/sub/module.txt by adding a line containing exactly
|
|
16
|
+
"feature-ready". Commit the submodule change, make that new submodule commit reachable
|
|
17
|
+
outside this linked worktree using the submodule's origin remote, then commit the
|
|
18
|
+
updated gitlink in super-wt. Do not rebuild the fixture and do not remove any worktree.
|
|
19
|
+
max_turns: 24
|
|
20
|
+
expectations:
|
|
21
|
+
- type: file_contains
|
|
22
|
+
path: super-wt/libs/sub/module.txt
|
|
23
|
+
value: feature-ready
|
|
24
|
+
description: requested submodule content exists
|
|
25
|
+
- type: command_exit_0
|
|
26
|
+
command: node .fixture-tools/verify-worktree-submodule.js
|
|
27
|
+
description: submodule is on a named branch, remotely reachable, clean, and recorded by superproject HEAD
|
|
28
|
+
- type: command_exit_0
|
|
29
|
+
command: node .fixture-tools/submodule-reachability.js --json --root super-wt
|
|
30
|
+
description: canonical #243 reachability helper accepts the final topology
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
function git(cwd, args) {
|
|
8
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8', windowsHide: true });
|
|
9
|
+
if (result.status !== 0) {
|
|
10
|
+
throw new Error((result.stderr || result.stdout || `git ${args.join(' ')} failed`).trim());
|
|
11
|
+
}
|
|
12
|
+
return result.stdout.trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fail(message) {
|
|
16
|
+
console.error(message);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const workspace = process.cwd();
|
|
22
|
+
const superRepo = path.join(workspace, 'super-wt');
|
|
23
|
+
const submodule = path.join(superRepo, 'libs', 'sub');
|
|
24
|
+
const content = fs.readFileSync(path.join(submodule, 'module.txt'), 'utf8');
|
|
25
|
+
if (!content.includes('feature-ready')) fail('submodule content was not updated');
|
|
26
|
+
|
|
27
|
+
const sha = git(submodule, ['rev-parse', 'HEAD']);
|
|
28
|
+
const branch = git(submodule, ['branch', '--show-current']);
|
|
29
|
+
if (!branch) fail('submodule commit remains detached');
|
|
30
|
+
|
|
31
|
+
const remoteRefs = git(submodule, [
|
|
32
|
+
'for-each-ref', '--contains', sha, '--format=%(refname:short)', 'refs/remotes/'
|
|
33
|
+
]).split(/\r?\n/).filter(Boolean);
|
|
34
|
+
if (remoteRefs.length === 0) fail('submodule commit is not published to a remote-tracking ref');
|
|
35
|
+
|
|
36
|
+
if (git(submodule, ['status', '--porcelain'])) fail('submodule working tree is not clean');
|
|
37
|
+
if (git(superRepo, ['status', '--porcelain', '--', 'libs/sub'])) fail('superproject gitlink is not clean');
|
|
38
|
+
|
|
39
|
+
const tree = git(superRepo, ['ls-tree', 'HEAD', 'libs/sub']);
|
|
40
|
+
const match = tree.match(/^160000 commit ([0-9a-f]{40,64})\tlibs\/sub$/);
|
|
41
|
+
if (!match || match[1] !== sha) fail('superproject HEAD does not record the new submodule commit');
|
|
42
|
+
|
|
43
|
+
console.log(JSON.stringify({ ok: true, sha, branch, remoteRefs }));
|
|
44
|
+
} catch (error) {
|
|
45
|
+
fail(error.message);
|
|
46
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
8
|
+
const FIXED_GIT_DATE = '2000-01-01T00:00:00Z';
|
|
9
|
+
|
|
10
|
+
function run(command, args, cwd) {
|
|
11
|
+
const result = spawnSync(command, args, {
|
|
12
|
+
cwd,
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
env: {
|
|
15
|
+
...process.env,
|
|
16
|
+
GIT_AUTHOR_DATE: FIXED_GIT_DATE,
|
|
17
|
+
GIT_COMMITTER_DATE: FIXED_GIT_DATE,
|
|
18
|
+
},
|
|
19
|
+
windowsHide: true,
|
|
20
|
+
});
|
|
21
|
+
if (result.status !== 0) {
|
|
22
|
+
throw new Error([
|
|
23
|
+
`${command} ${args.join(' ')} failed with ${result.status}`,
|
|
24
|
+
result.stdout,
|
|
25
|
+
result.stderr,
|
|
26
|
+
].filter(Boolean).join('\n'));
|
|
27
|
+
}
|
|
28
|
+
return result.stdout.trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function git(cwd, args) {
|
|
32
|
+
return run('git', args, cwd);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function initRepo(dir) {
|
|
36
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
37
|
+
git(dir, ['init', '-b', 'main']);
|
|
38
|
+
git(dir, ['config', 'user.name', 'Harness Behavioral Fixture']);
|
|
39
|
+
git(dir, ['config', 'user.email', 'behavioral-fixture@example.invalid']);
|
|
40
|
+
git(dir, ['config', 'commit.gpgSign', 'false']);
|
|
41
|
+
git(dir, ['config', 'core.autocrlf', 'false']);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const workspace = process.cwd();
|
|
45
|
+
const child = path.join(workspace, 'child-origin');
|
|
46
|
+
const superRepo = path.join(workspace, 'super');
|
|
47
|
+
const worktree = path.join(workspace, 'super-wt');
|
|
48
|
+
const toolsDir = path.join(workspace, '.fixture-tools');
|
|
49
|
+
|
|
50
|
+
initRepo(child);
|
|
51
|
+
fs.writeFileSync(path.join(child, 'module.txt'), 'seed\n');
|
|
52
|
+
git(child, ['add', 'module.txt']);
|
|
53
|
+
git(child, ['commit', '-m', 'seed child']);
|
|
54
|
+
|
|
55
|
+
initRepo(superRepo);
|
|
56
|
+
fs.writeFileSync(path.join(superRepo, 'README.md'), 'superproject fixture\n');
|
|
57
|
+
git(superRepo, ['add', 'README.md']);
|
|
58
|
+
git(superRepo, ['commit', '-m', 'seed superproject']);
|
|
59
|
+
// Keep the committed .gitmodules URL independent of the random temp workspace.
|
|
60
|
+
git(superRepo, ['-c', 'protocol.file.allow=always', 'submodule', 'add', '../child-origin', 'libs/sub']);
|
|
61
|
+
git(superRepo, ['commit', '-am', 'add child submodule']);
|
|
62
|
+
|
|
63
|
+
git(superRepo, ['worktree', 'add', worktree, '-b', 'eval/submodule-reachability']);
|
|
64
|
+
git(worktree, ['-c', 'protocol.file.allow=always', 'submodule', 'update', '--init']);
|
|
65
|
+
const linkedSub = path.join(worktree, 'libs', 'sub');
|
|
66
|
+
git(linkedSub, ['config', 'user.name', 'Harness Behavioral Agent']);
|
|
67
|
+
git(linkedSub, ['config', 'user.email', 'behavioral-agent@example.invalid']);
|
|
68
|
+
|
|
69
|
+
fs.mkdirSync(toolsDir, { recursive: true });
|
|
70
|
+
fs.copyFileSync(
|
|
71
|
+
path.join(ROOT, 'using-git-worktrees', 'scripts', 'submodule-reachability.js'),
|
|
72
|
+
path.join(toolsDir, 'submodule-reachability.js')
|
|
73
|
+
);
|
|
74
|
+
fs.copyFileSync(
|
|
75
|
+
path.join(__dirname, 'verify-worktree-submodule.js'),
|
|
76
|
+
path.join(toolsDir, 'verify-worktree-submodule.js')
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
fs.writeFileSync(
|
|
80
|
+
path.join(workspace, '.fixture-topology.json'),
|
|
81
|
+
JSON.stringify({
|
|
82
|
+
primary: 'super',
|
|
83
|
+
linked_worktree: 'super-wt',
|
|
84
|
+
submodule: 'super-wt/libs/sub',
|
|
85
|
+
}, null, 2) + '\n'
|
|
86
|
+
);
|
|
@@ -7,11 +7,12 @@ const path = require('path');
|
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
const { execFileSync, spawnSync } = require('child_process');
|
|
9
9
|
const {
|
|
10
|
-
|
|
10
|
+
prepareWorkspace,
|
|
11
11
|
grade,
|
|
12
12
|
parseSimpleYaml,
|
|
13
13
|
} = require('./run');
|
|
14
14
|
const { verifyEvidence: verifyOpenCodeReflectionEvidence } = require('./opencode-reflection-gate-live');
|
|
15
|
+
const { resolveFixtureSetup } = require('./case-validator');
|
|
15
16
|
|
|
16
17
|
const ROOT = path.resolve(__dirname, '..');
|
|
17
18
|
const CASES_DIR = path.join(__dirname, 'cases');
|
|
@@ -40,6 +41,11 @@ function stableJson(value) {
|
|
|
40
41
|
return JSON.stringify(stable(value));
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
function fixtureSetupSha256(c) {
|
|
45
|
+
const resolved = resolveFixtureSetup(c && c.fixture && c.fixture.setup);
|
|
46
|
+
return resolved ? sha256File(resolved.absolute) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
function discoverCases() {
|
|
44
50
|
return fs.readdirSync(CASES_DIR)
|
|
45
51
|
.filter((file) => file.endsWith('.yaml'))
|
|
@@ -216,7 +222,9 @@ function retrieveLesson(c, memoryStore, stateHome, exposed) {
|
|
|
216
222
|
}
|
|
217
223
|
|
|
218
224
|
function prepareArmWorkspace(c, effectType, arm, engine) {
|
|
219
|
-
const
|
|
225
|
+
const prepared = prepareWorkspace(c);
|
|
226
|
+
const ws = prepared.workspace;
|
|
227
|
+
if (!prepared.ok) return { ...prepared, ws, fixture_setup: prepared.setup };
|
|
220
228
|
const namedSkills = treatmentSkills(c);
|
|
221
229
|
const treatment = arm === 'treatment';
|
|
222
230
|
let skills = [];
|
|
@@ -245,6 +253,8 @@ function prepareArmWorkspace(c, effectType, arm, engine) {
|
|
|
245
253
|
}
|
|
246
254
|
|
|
247
255
|
return {
|
|
256
|
+
ok: true,
|
|
257
|
+
fixture_setup: prepared.setup,
|
|
248
258
|
ws,
|
|
249
259
|
namedSkills,
|
|
250
260
|
loadedSkills: skills,
|
|
@@ -367,8 +377,25 @@ function usageTotal(usage) {
|
|
|
367
377
|
return typeof input === 'number' && typeof output === 'number' ? input + output : null;
|
|
368
378
|
}
|
|
369
379
|
|
|
370
|
-
function runArm(c, context, arm, pairDir) {
|
|
380
|
+
function runArm(c, context, arm, pairDir, execute = spawnSync) {
|
|
371
381
|
const prepared = prepareArmWorkspace(c, context.effect_type, arm, context.engine);
|
|
382
|
+
if (!prepared.ok) {
|
|
383
|
+
if (prepared.ws) fs.rmSync(prepared.ws, { recursive: true, force: true });
|
|
384
|
+
return {
|
|
385
|
+
arm,
|
|
386
|
+
outcome: 'fixture-error',
|
|
387
|
+
fixture_setup: prepared.fixture_setup,
|
|
388
|
+
infrastructure_reason: prepared.error,
|
|
389
|
+
loaded_skills: [],
|
|
390
|
+
model_name: null,
|
|
391
|
+
cost: null,
|
|
392
|
+
usage: null,
|
|
393
|
+
total_tokens: null,
|
|
394
|
+
tool_call_count: null,
|
|
395
|
+
duration_ms: null,
|
|
396
|
+
expectations: [],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
372
399
|
const stateHome = fs.mkdtempSync(path.join(os.tmpdir(), `harness-paired-state-${arm}-`));
|
|
373
400
|
const transcriptFile = path.join(pairDir, `${arm}.transcript.jsonl`);
|
|
374
401
|
const stderrFile = path.join(pairDir, `${arm}.stderr.txt`);
|
|
@@ -388,6 +415,7 @@ function runArm(c, context, arm, pairDir) {
|
|
|
388
415
|
fs.rmSync(stateHome, { recursive: true, force: true });
|
|
389
416
|
return {
|
|
390
417
|
arm,
|
|
418
|
+
fixture_setup: prepared.fixture_setup,
|
|
391
419
|
intervention: prepared.intervention,
|
|
392
420
|
loaded_skills: prepared.loadedSkills,
|
|
393
421
|
plugin_sha256: null,
|
|
@@ -404,7 +432,7 @@ function runArm(c, context, arm, pairDir) {
|
|
|
404
432
|
}
|
|
405
433
|
const invocation = buildInvocation(context.engine, context.model, armPrompt, prepared.ws, c.max_turns);
|
|
406
434
|
const started = Date.now();
|
|
407
|
-
const result =
|
|
435
|
+
const result = execute(invocation.command, invocation.args, {
|
|
408
436
|
cwd: prepared.ws,
|
|
409
437
|
env: { ...process.env, HARNESS_STATE_HOME: stateHome },
|
|
410
438
|
encoding: 'utf8',
|
|
@@ -420,6 +448,7 @@ function runArm(c, context, arm, pairDir) {
|
|
|
420
448
|
|
|
421
449
|
const base = {
|
|
422
450
|
arm,
|
|
451
|
+
fixture_setup: prepared.fixture_setup,
|
|
423
452
|
intervention: prepared.intervention,
|
|
424
453
|
loaded_skills: prepared.loadedSkills,
|
|
425
454
|
plugin_sha256: prepared.plugin ? prepared.plugin.sha256 : null,
|
|
@@ -566,6 +595,7 @@ function pairContract(c, context) {
|
|
|
566
595
|
max_turns: c.max_turns,
|
|
567
596
|
loaded_skills: treatmentSkills(c).slice().sort(),
|
|
568
597
|
fixture_sha256: sha256(stableJson(c.fixture)),
|
|
598
|
+
fixture_setup_sha256: fixtureSetupSha256(c),
|
|
569
599
|
prompt_sha256: sha256(c.prompt),
|
|
570
600
|
rubric_sha256: sha256(stableJson(c.expectations)),
|
|
571
601
|
lesson_fixture_sha256: context.effect_type === 'lesson-retrieval' ? sha256(stableJson(c.lesson)) : null,
|
|
@@ -781,6 +811,8 @@ function summarizePairs(records, minEffectPp) {
|
|
|
781
811
|
requested_pairs: records.length,
|
|
782
812
|
included_pairs: included.length,
|
|
783
813
|
excluded_pairs: excluded.length,
|
|
814
|
+
fixture_failures: records.filter(record => Object.values(record.arms || {})
|
|
815
|
+
.some(arm => arm.outcome === 'fixture-error')).length,
|
|
784
816
|
exclusions: excluded,
|
|
785
817
|
arm_outcomes: {
|
|
786
818
|
baseline: { pass: baselinePass, fail: included.length - baselinePass },
|
|
@@ -941,7 +973,8 @@ function main(argv = process.argv.slice(2)) {
|
|
|
941
973
|
min_effect_pp: Number(flag(argv, '--min-effect-pp')),
|
|
942
974
|
preflight: flag(argv, '--opencode-preflight'),
|
|
943
975
|
};
|
|
944
|
-
runExperiment(options);
|
|
976
|
+
const { summary } = runExperiment(options);
|
|
977
|
+
if (summary.fixture_failures) process.exitCode = 1;
|
|
945
978
|
return;
|
|
946
979
|
}
|
|
947
980
|
if (command === 'summarize') {
|
|
@@ -978,6 +1011,7 @@ module.exports = {
|
|
|
978
1011
|
lessonContextFromRetrieval,
|
|
979
1012
|
lessonFixtureRecord,
|
|
980
1013
|
prepareArmWorkspace,
|
|
1014
|
+
runArm,
|
|
981
1015
|
retrieveLesson,
|
|
982
1016
|
summarizePairs,
|
|
983
1017
|
validateLessonCase,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const { execFileSync } = require('child_process');
|
|
12
|
-
const {
|
|
12
|
+
const { prepareWorkspace, validate: validateCases } = require('./run');
|
|
13
13
|
|
|
14
14
|
const ROOT = path.resolve(__dirname, '..');
|
|
15
15
|
const CASES_DIR = path.join(__dirname, 'cases');
|
|
@@ -243,6 +243,36 @@ function grade(c, ws, transcriptPath) {
|
|
|
243
243
|
return { results, passed: gating.length > 0 && gating.every((g) => g.pass) };
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
function runCase(c, execute = runHeadless) {
|
|
247
|
+
const prepared = prepareWorkspace(c);
|
|
248
|
+
const ws = prepared.workspace;
|
|
249
|
+
const record = {
|
|
250
|
+
id: c.id,
|
|
251
|
+
engine: 'opencode-with-plugin',
|
|
252
|
+
model: process.env.BEHAVIORAL_MODEL || 'openai/gpt-5-mini',
|
|
253
|
+
pressure: !!c.pressure,
|
|
254
|
+
date: new Date().toISOString(),
|
|
255
|
+
workspace: ws,
|
|
256
|
+
fixture_setup: prepared.setup,
|
|
257
|
+
expectations: [],
|
|
258
|
+
};
|
|
259
|
+
if (!prepared.ok) return { ...record, outcome: 'fixture-error', error: prepared.error };
|
|
260
|
+
try {
|
|
261
|
+
installHarnessWithPlugin(ws);
|
|
262
|
+
if (c.fixture.git) gitSnapshot(ws);
|
|
263
|
+
const transcriptPath = execute(c.prompt, ws, c.max_turns);
|
|
264
|
+
const { results, passed } = grade(c, ws, transcriptPath);
|
|
265
|
+
return {
|
|
266
|
+
...record,
|
|
267
|
+
transcript: transcriptPath,
|
|
268
|
+
expectations: results.map(({ description, pass, informational }) => ({ description, pass, ...(informational ? { informational: true } : {}) })),
|
|
269
|
+
outcome: passed ? 'pass' : 'fail',
|
|
270
|
+
};
|
|
271
|
+
} catch (error) {
|
|
272
|
+
return { ...record, outcome: 'session-error', error: error.message.slice(0, 500) };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
246
276
|
function runLive(filter) {
|
|
247
277
|
console.log('Engine: opencode (with plugin hooks)');
|
|
248
278
|
const cases = discoverCases().filter((c) => !filter || c.id === filter);
|
|
@@ -252,49 +282,33 @@ function runLive(filter) {
|
|
|
252
282
|
let failed = 0;
|
|
253
283
|
for (const c of cases) {
|
|
254
284
|
console.log(`\n=== ${c.id}${c.pressure ? ' [PRESSURE]' : ''} ===`);
|
|
255
|
-
const
|
|
256
|
-
installHarnessWithPlugin(ws);
|
|
257
|
-
if (c.fixture.git) gitSnapshot(ws);
|
|
258
|
-
let transcriptPath;
|
|
259
|
-
try {
|
|
260
|
-
transcriptPath = runHeadless(c.prompt, ws, c.max_turns);
|
|
261
|
-
} catch (err) {
|
|
262
|
-
console.error(`❌ session failed: ${err.message.slice(0, 300)}`);
|
|
263
|
-
failed++;
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
const { results: graded, passed } = grade(c, ws, transcriptPath);
|
|
267
|
-
const record = {
|
|
268
|
-
id: c.id,
|
|
269
|
-
engine: 'opencode-with-plugin',
|
|
270
|
-
model: process.env.BEHAVIORAL_MODEL || 'openai/gpt-5-mini',
|
|
271
|
-
pressure: !!c.pressure,
|
|
272
|
-
date: new Date().toISOString(),
|
|
273
|
-
workspace: ws,
|
|
274
|
-
transcript: transcriptPath,
|
|
275
|
-
expectations: graded.map(({ description, pass }) => ({ description, pass })),
|
|
276
|
-
outcome: passed ? 'pass' : 'fail',
|
|
277
|
-
};
|
|
285
|
+
const record = runCase(c);
|
|
278
286
|
const outFile = path.join(RESULTS_DIR, `plugin-${record.date.slice(0, 10)}-${c.id}.json`);
|
|
279
287
|
fs.writeFileSync(outFile, JSON.stringify(record, null, 2));
|
|
280
|
-
for (const g of
|
|
281
|
-
console.log(`${
|
|
282
|
-
if (
|
|
288
|
+
for (const g of record.expectations) console.log(` ${g.pass ? '✅' : '❌'}${g.informational ? ' (info)' : ''} ${g.description}`);
|
|
289
|
+
console.log(`${record.outcome} -> ${outFile}`);
|
|
290
|
+
if (record.error) console.error(record.error);
|
|
291
|
+
if (record.outcome !== 'pass') failed++;
|
|
283
292
|
}
|
|
284
293
|
console.log(failed ? `\n❌ ${failed} case(s) failed.` : '\n🎉 all cases passed.');
|
|
285
294
|
process.exit(failed ? 1 : 0);
|
|
286
295
|
}
|
|
287
296
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
297
|
+
function main() {
|
|
298
|
+
const args = process.argv.slice(2);
|
|
299
|
+
function flag(name) {
|
|
300
|
+
const i = args.indexOf(name);
|
|
301
|
+
return i >= 0 ? args[i + 1] : undefined;
|
|
302
|
+
}
|
|
303
|
+
if (args[0] === 'validate') validateCases(discoverCases());
|
|
304
|
+
else if (args[0] === 'run') {
|
|
305
|
+
const filter = args.includes('--case') ? flag('--case') : undefined;
|
|
306
|
+
runLive(filter);
|
|
307
|
+
} else {
|
|
308
|
+
console.log('Usage:\n node behavioral-evals/run-with-plugin.js run [--case <id>]');
|
|
309
|
+
process.exit(args.length ? 1 : 0);
|
|
310
|
+
}
|
|
300
311
|
}
|
|
312
|
+
|
|
313
|
+
if (require.main === module) main();
|
|
314
|
+
module.exports = { runCase };
|
package/behavioral-evals/run.js
CHANGED
|
@@ -22,12 +22,13 @@ const fs = require('fs');
|
|
|
22
22
|
const os = require('os');
|
|
23
23
|
const path = require('path');
|
|
24
24
|
const crypto = require('crypto');
|
|
25
|
-
const { execFileSync } = require('child_process');
|
|
25
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
26
26
|
const {
|
|
27
27
|
gradeExecutionEvidence,
|
|
28
28
|
parseTranscriptFile,
|
|
29
29
|
} = require('./transcript-parser');
|
|
30
30
|
const { commandMatches, normalizeRepoPath } = require('../scripts/lib/execution-contract');
|
|
31
|
+
const { resolveFixtureSetup, validateFixture } = require('./case-validator');
|
|
31
32
|
|
|
32
33
|
const ROOT = path.resolve(__dirname, '..');
|
|
33
34
|
const CASES_DIR = path.join(__dirname, 'cases');
|
|
@@ -155,7 +156,8 @@ function validate(cases) {
|
|
|
155
156
|
if (!c.id) problems.push('missing id');
|
|
156
157
|
if (!c.prompt) problems.push('missing prompt');
|
|
157
158
|
if (!c.max_turns) problems.push('missing max_turns');
|
|
158
|
-
if (!c.fixture
|
|
159
|
+
if (!c.fixture) problems.push('fixture missing');
|
|
160
|
+
else problems.push(...validateFixture(c.fixture));
|
|
159
161
|
if (!Array.isArray(c.expectations) || c.expectations.length === 0) problems.push('expectations missing');
|
|
160
162
|
if (c.loaded_skills !== undefined && (!Array.isArray(c.loaded_skills) || c.loaded_skills.some(skill => !fs.existsSync(path.join(ROOT, skill, 'SKILL.md'))))) {
|
|
161
163
|
problems.push('loaded_skills must name existing skills');
|
|
@@ -207,6 +209,66 @@ function buildWorkspace(c) {
|
|
|
207
209
|
return ws;
|
|
208
210
|
}
|
|
209
211
|
|
|
212
|
+
function runFixtureSetup(c, ws) {
|
|
213
|
+
const resolved = resolveFixtureSetup(c && c.fixture && c.fixture.setup);
|
|
214
|
+
if (!resolved) return { path: null, script_sha256: null, status: 'not-requested', exit_code: null, duration_ms: 0 };
|
|
215
|
+
const scriptSha256 = sha256(fs.readFileSync(resolved.absolute));
|
|
216
|
+
const startedAt = Date.now();
|
|
217
|
+
const result = spawnSync(process.execPath, [resolved.absolute], {
|
|
218
|
+
cwd: ws,
|
|
219
|
+
encoding: 'utf8',
|
|
220
|
+
env: {
|
|
221
|
+
...process.env,
|
|
222
|
+
HARNESS_BEHAVIORAL_WORKSPACE: ws,
|
|
223
|
+
HARNESS_BEHAVIORAL_CASE_ID: String(c.id || ''),
|
|
224
|
+
},
|
|
225
|
+
timeout: 60 * 1000,
|
|
226
|
+
windowsHide: true,
|
|
227
|
+
});
|
|
228
|
+
const evidence = {
|
|
229
|
+
path: resolved.relative,
|
|
230
|
+
script_sha256: scriptSha256,
|
|
231
|
+
status: result.status === 0 && !result.error ? 'pass' : 'fail',
|
|
232
|
+
exit_code: result.status,
|
|
233
|
+
duration_ms: Date.now() - startedAt,
|
|
234
|
+
};
|
|
235
|
+
if (result.error || result.status !== 0) {
|
|
236
|
+
const detail = (result.stderr || result.stdout || (result.error && result.error.message) || '').trim();
|
|
237
|
+
const error = new Error(
|
|
238
|
+
`fixture setup failed: ${resolved.relative}${result.status === null ? '' : ` (exit ${result.status})`}${detail ? `: ${detail.slice(0, 300)}` : ''}`
|
|
239
|
+
);
|
|
240
|
+
error.fixtureSetup = evidence;
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
return evidence;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function prepareWorkspace(c) {
|
|
247
|
+
let workspace = null;
|
|
248
|
+
try {
|
|
249
|
+
workspace = buildWorkspace(c);
|
|
250
|
+
return {
|
|
251
|
+
ok: true,
|
|
252
|
+
workspace,
|
|
253
|
+
setup: runFixtureSetup(c, workspace),
|
|
254
|
+
};
|
|
255
|
+
} catch (error) {
|
|
256
|
+
return {
|
|
257
|
+
ok: false,
|
|
258
|
+
workspace,
|
|
259
|
+
outcome: 'fixture-error',
|
|
260
|
+
error: error.message.slice(0, 500),
|
|
261
|
+
setup: error.fixtureSetup || {
|
|
262
|
+
path: c && c.fixture ? c.fixture.setup || null : null,
|
|
263
|
+
script_sha256: null,
|
|
264
|
+
status: 'fail',
|
|
265
|
+
exit_code: null,
|
|
266
|
+
duration_ms: null,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
210
272
|
// Capture the requested fixture baseline after treatment installation so the
|
|
211
273
|
// installer's own artifacts (.claude/, .harness-src/, opencode.json) show up
|
|
212
274
|
// in neither arm's post-run scope check.
|
|
@@ -685,9 +747,8 @@ function treatmentSkills(c) {
|
|
|
685
747
|
function runArm(c, engine, arm, pairId) {
|
|
686
748
|
const loadedSkills = arm === 'treatment' ? treatmentSkills(c) : [];
|
|
687
749
|
const attributionRequired = arm === 'treatment';
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
if (c.fixture.git) gitSnapshot(ws);
|
|
750
|
+
const prepared = prepareWorkspace(c);
|
|
751
|
+
const ws = prepared.workspace;
|
|
691
752
|
|
|
692
753
|
const record = {
|
|
693
754
|
arm,
|
|
@@ -698,9 +759,22 @@ function runArm(c, engine, arm, pairId) {
|
|
|
698
759
|
workspace: ws,
|
|
699
760
|
fixture_sha256: sha256(JSON.stringify(c.fixture)),
|
|
700
761
|
prompt_sha256: sha256(c.prompt),
|
|
762
|
+
fixture_setup: prepared.setup,
|
|
701
763
|
cost: null,
|
|
702
764
|
};
|
|
765
|
+
if (!prepared.ok) {
|
|
766
|
+
return {
|
|
767
|
+
...record,
|
|
768
|
+
outcome: 'fixture-error',
|
|
769
|
+
error: prepared.error,
|
|
770
|
+
parse_status: 'unavailable',
|
|
771
|
+
tool_call_count: null,
|
|
772
|
+
tool_call_counts: { attempted: null, completed: null, denied: null, unresolved: null },
|
|
773
|
+
};
|
|
774
|
+
}
|
|
703
775
|
try {
|
|
776
|
+
if (arm === 'treatment') installHarness(ws, loadedSkills);
|
|
777
|
+
if (c.fixture.git) gitSnapshot(ws);
|
|
704
778
|
const transcriptPath = runHeadless(c.prompt, ws, c.max_turns, engine, arm);
|
|
705
779
|
const graded = grade(c, ws, transcriptPath, engine);
|
|
706
780
|
const { results, passed } = graded;
|
|
@@ -764,6 +838,8 @@ function summarizePairResults(pairResults) {
|
|
|
764
838
|
const completed = pairResults.filter(isCompletedPair);
|
|
765
839
|
const sessionFailures = pairResults.filter(record => Object.values(record.arms || {})
|
|
766
840
|
.some(arm => arm.outcome === 'session-error')).length;
|
|
841
|
+
const fixtureFailures = pairResults.filter(record => Object.values(record.arms || {})
|
|
842
|
+
.some(arm => arm.outcome === 'fixture-error')).length;
|
|
767
843
|
const effective = completed.filter(record => record.verdict === 'EFFECTIVE').length;
|
|
768
844
|
const categorySummary = {};
|
|
769
845
|
for (const record of pairResults) {
|
|
@@ -806,6 +882,7 @@ function summarizePairResults(pairResults) {
|
|
|
806
882
|
cost,
|
|
807
883
|
completed_pairs: completed.length,
|
|
808
884
|
session_failures: sessionFailures,
|
|
885
|
+
fixture_failures: fixtureFailures,
|
|
809
886
|
tool_call_delta: toolCallDelta,
|
|
810
887
|
verdicts: Object.fromEntries(['EFFECTIVE', 'INEFFECTIVE', 'INCONCLUSIVE', 'HARMFUL']
|
|
811
888
|
.map(verdict => [verdict, completed.filter(record => record.verdict === verdict).length])),
|
|
@@ -835,7 +912,7 @@ function runLive(filter, engineArg, armArg = 'treatment') {
|
|
|
835
912
|
fs.mkdirSync(RESULTS_DIR, { recursive: true });
|
|
836
913
|
|
|
837
914
|
const pairResults = [];
|
|
838
|
-
let
|
|
915
|
+
let infrastructureFailures = 0;
|
|
839
916
|
for (const c of cases) {
|
|
840
917
|
const pairId = `${c.id}-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
|
|
841
918
|
const order = armArg === 'both'
|
|
@@ -864,13 +941,13 @@ function runLive(filter, engineArg, armArg = 'treatment') {
|
|
|
864
941
|
for (const arm of Object.values(arms)) {
|
|
865
942
|
console.log(` ${arm.arm}: ${arm.outcome} (${arm.loaded_skills.length ? arm.loaded_skills.join(', ') : 'no Harness skills'})`);
|
|
866
943
|
}
|
|
867
|
-
if (Object.values(arms).some(arm => arm.outcome === 'session-error'))
|
|
944
|
+
if (Object.values(arms).some(arm => arm.outcome === 'session-error' || arm.outcome === 'fixture-error')) infrastructureFailures++;
|
|
868
945
|
if (armArg === 'both') console.log(` Verdict: ${record.verdict}`);
|
|
869
946
|
const suffix = armArg === 'both' ? 'pair' : armArg;
|
|
870
947
|
fs.writeFileSync(path.join(RESULTS_DIR, `${record.date.slice(0, 10)}-${c.id}-${suffix}.json`), JSON.stringify(record, null, 2));
|
|
871
948
|
}
|
|
872
949
|
|
|
873
|
-
let exitCode =
|
|
950
|
+
let exitCode = infrastructureFailures ? 1 : 0;
|
|
874
951
|
if (armArg === 'both') {
|
|
875
952
|
const aggregate = summarizePairResults(pairResults);
|
|
876
953
|
const summary = {
|
|
@@ -888,7 +965,7 @@ function runLive(filter, engineArg, armArg = 'treatment') {
|
|
|
888
965
|
fs.writeFileSync(summaryPath, JSON.stringify(summary, null, 2));
|
|
889
966
|
console.log(`\nPaired summary: ${summaryPath}`);
|
|
890
967
|
console.log(`Completed pairs: ${summary.completed_pairs}/${summary.requested_sample_size}`);
|
|
891
|
-
exitCode = aggregate.session_failures ? 1 : 0;
|
|
968
|
+
exitCode = (aggregate.session_failures || aggregate.fixture_failures) ? 1 : 0;
|
|
892
969
|
}
|
|
893
970
|
process.exit(exitCode);
|
|
894
971
|
}
|
|
@@ -915,6 +992,8 @@ if (require.main === module) main();
|
|
|
915
992
|
module.exports = {
|
|
916
993
|
buildEngineInvocation,
|
|
917
994
|
buildWorkspace,
|
|
995
|
+
runFixtureSetup,
|
|
996
|
+
prepareWorkspace,
|
|
918
997
|
countToolCalls,
|
|
919
998
|
extractAgentEvents,
|
|
920
999
|
extractAgentTrace,
|
|
@@ -2,7 +2,8 @@ const helper = require('./test-helper');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const {
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const { buildEngineInvocation, buildWorkspace, runFixtureSetup, prepareWorkspace, summarizePairResults } = require('../behavioral-evals/run');
|
|
6
7
|
|
|
7
8
|
console.log('\n[2j] Behavioral runner argv integrity...');
|
|
8
9
|
const prompt = "Add punctuation stripping to slug.js, then run npm test. Do not truncate this request.";
|
|
@@ -59,4 +60,123 @@ helper.check(
|
|
|
59
60
|
traversalError && traversalError.stack ? traversalError.stack : traversalTarget
|
|
60
61
|
);
|
|
61
62
|
|
|
63
|
+
let setupWorkspace = null;
|
|
64
|
+
try {
|
|
65
|
+
setupWorkspace = buildWorkspace({
|
|
66
|
+
id: 'fixture-setup-valid',
|
|
67
|
+
fixture: {
|
|
68
|
+
files: [{ path: 'seed.txt', content: 'seed' }],
|
|
69
|
+
setup: 'behavioral-evals/fixtures/setup/noop.js',
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
const setupResult = runFixtureSetup({
|
|
73
|
+
id: 'fixture-setup-valid',
|
|
74
|
+
fixture: { setup: 'behavioral-evals/fixtures/setup/noop.js' },
|
|
75
|
+
}, setupWorkspace);
|
|
76
|
+
helper.check(
|
|
77
|
+
'2j. fixture setup runs via Node in the generated workspace',
|
|
78
|
+
setupResult.status === 'pass' &&
|
|
79
|
+
fs.readFileSync(path.join(setupWorkspace, '.fixture-setup-ok'), 'utf8') === 'ok\n',
|
|
80
|
+
JSON.stringify(setupResult)
|
|
81
|
+
);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
helper.check('2j. fixture setup runs via Node in the generated workspace', false, error && error.stack ? error.stack : String(error));
|
|
84
|
+
} finally {
|
|
85
|
+
if (setupWorkspace) fs.rmSync(setupWorkspace, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let failedPreparation = null;
|
|
89
|
+
try {
|
|
90
|
+
failedPreparation = prepareWorkspace({
|
|
91
|
+
id: 'fixture-setup-fails',
|
|
92
|
+
fixture: {
|
|
93
|
+
files: [{ path: 'seed.txt', content: 'seed' }],
|
|
94
|
+
setup: 'behavioral-evals/fixtures/setup/fail.js',
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
} catch (error) {
|
|
98
|
+
failedPreparation = { threw: error && error.stack ? error.stack : String(error) };
|
|
99
|
+
}
|
|
100
|
+
helper.check(
|
|
101
|
+
'2j. setup failure is classified as fixture-error before any model session',
|
|
102
|
+
Boolean(
|
|
103
|
+
failedPreparation &&
|
|
104
|
+
failedPreparation.ok === false &&
|
|
105
|
+
failedPreparation.outcome === 'fixture-error' &&
|
|
106
|
+
failedPreparation.setup &&
|
|
107
|
+
failedPreparation.setup.status === 'fail' &&
|
|
108
|
+
failedPreparation.setup.exit_code === 17
|
|
109
|
+
),
|
|
110
|
+
JSON.stringify(failedPreparation)
|
|
111
|
+
);
|
|
112
|
+
if (failedPreparation && failedPreparation.workspace) {
|
|
113
|
+
fs.rmSync(failedPreparation.workspace, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let topologyPreparation = null;
|
|
117
|
+
try {
|
|
118
|
+
topologyPreparation = prepareWorkspace({
|
|
119
|
+
id: 'worktree-submodule-setup-smoke',
|
|
120
|
+
fixture: {
|
|
121
|
+
files: [{ path: 'README.md', content: 'topology smoke' }],
|
|
122
|
+
setup: 'behavioral-evals/fixtures/setup/worktree-submodule.js',
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
const linkedSub = topologyPreparation.ok
|
|
126
|
+
? path.join(topologyPreparation.workspace, 'super-wt', 'libs', 'sub')
|
|
127
|
+
: null;
|
|
128
|
+
const branchResult = linkedSub
|
|
129
|
+
? spawnSync('git', ['-C', linkedSub, 'branch', '--show-current'], { encoding: 'utf8', windowsHide: true })
|
|
130
|
+
: null;
|
|
131
|
+
const helperResult = topologyPreparation.ok
|
|
132
|
+
? spawnSync(process.execPath, [
|
|
133
|
+
path.join(topologyPreparation.workspace, '.fixture-tools', 'submodule-reachability.js'),
|
|
134
|
+
'--json', '--root', 'super-wt',
|
|
135
|
+
], { cwd: topologyPreparation.workspace, encoding: 'utf8', windowsHide: true })
|
|
136
|
+
: null;
|
|
137
|
+
helper.check(
|
|
138
|
+
'2j. worktree/submodule setup creates the real #243 topology before model execution',
|
|
139
|
+
Boolean(
|
|
140
|
+
topologyPreparation.ok &&
|
|
141
|
+
fs.existsSync(path.join(topologyPreparation.workspace, 'super-wt', '.git')) &&
|
|
142
|
+
fs.existsSync(path.join(linkedSub, '.git')) &&
|
|
143
|
+
branchResult && branchResult.status === 0 && branchResult.stdout.trim() === '' &&
|
|
144
|
+
helperResult && helperResult.status === 0
|
|
145
|
+
),
|
|
146
|
+
JSON.stringify({
|
|
147
|
+
preparation: topologyPreparation,
|
|
148
|
+
branchStatus: branchResult && branchResult.status,
|
|
149
|
+
branch: branchResult && branchResult.stdout,
|
|
150
|
+
helperStatus: helperResult && helperResult.status,
|
|
151
|
+
helperStderr: helperResult && helperResult.stderr,
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
helper.check(
|
|
156
|
+
'2j. worktree/submodule setup creates the real #243 topology before model execution',
|
|
157
|
+
false,
|
|
158
|
+
error && error.stack ? error.stack : String(error)
|
|
159
|
+
);
|
|
160
|
+
} finally {
|
|
161
|
+
if (topologyPreparation && topologyPreparation.workspace) {
|
|
162
|
+
fs.rmSync(topologyPreparation.workspace, { recursive: true, force: true });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const fixtureFailureSummary = summarizePairResults([{
|
|
167
|
+
id: 'fixture-error-pair',
|
|
168
|
+
pressure_category: null,
|
|
169
|
+
arms: {
|
|
170
|
+
baseline: { outcome: 'fixture-error', cost: null, tool_call_count: null },
|
|
171
|
+
treatment: { outcome: 'pass', cost: null, tool_call_count: 1 },
|
|
172
|
+
},
|
|
173
|
+
}]);
|
|
174
|
+
helper.check(
|
|
175
|
+
'2j. fixture errors remain distinct aggregate infrastructure failures',
|
|
176
|
+
fixtureFailureSummary.fixture_failures === 1 &&
|
|
177
|
+
fixtureFailureSummary.session_failures === 0 &&
|
|
178
|
+
fixtureFailureSummary.completed_pairs === 0,
|
|
179
|
+
JSON.stringify(fixtureFailureSummary)
|
|
180
|
+
);
|
|
181
|
+
|
|
62
182
|
helper.finish();
|
|
@@ -0,0 +1,153 @@
|
|
|
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
|
+
const { parseSimpleYaml, prepareWorkspace } = require('../behavioral-evals/run');
|
|
9
|
+
const paired = require('../behavioral-evals/paired-benchmark');
|
|
10
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
11
|
+
const topology = parseSimpleYaml(fs.readFileSync(path.join(ROOT, 'behavioral-evals/cases/baseline-worktree-submodule-reachability.yaml'), 'utf8'));
|
|
12
|
+
const failure = { ...topology, fixture: { ...topology.fixture, setup: 'behavioral-evals/fixtures/setup/fail.js' } };
|
|
13
|
+
const paths = [];
|
|
14
|
+
const topologyFingerprints = [];
|
|
15
|
+
const setupHashes = [];
|
|
16
|
+
function track(ws) { if (ws) paths.push(ws); return ws; }
|
|
17
|
+
function gitText(cwd, args) {
|
|
18
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8', windowsHide: true });
|
|
19
|
+
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
|
|
20
|
+
return result.stdout.trim();
|
|
21
|
+
}
|
|
22
|
+
function topologyFingerprint(ws) {
|
|
23
|
+
return {
|
|
24
|
+
child_head: gitText(path.join(ws, 'child-origin'), ['rev-parse', 'HEAD']),
|
|
25
|
+
super_head: gitText(path.join(ws, 'super'), ['rev-parse', 'HEAD']),
|
|
26
|
+
linked_head: gitText(path.join(ws, 'super-wt'), ['rev-parse', 'HEAD']),
|
|
27
|
+
gitlink: gitText(path.join(ws, 'super-wt'), ['rev-parse', 'HEAD:libs/sub']),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function assertTopology(ws) {
|
|
31
|
+
assert.ok(fs.existsSync(path.join(ws, 'super-wt', '.git')), 'setup must construct the linked worktree');
|
|
32
|
+
assert.ok(fs.existsSync(path.join(ws, '.fixture-tools', 'verify-worktree-submodule.js')), 'setup must install the grader');
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
for (const arm of ['baseline', 'treatment']) {
|
|
36
|
+
for (const effect of ['skill-text', 'plugin-enforcement']) {
|
|
37
|
+
const prepared = paired.prepareArmWorkspace(topology, effect, arm, 'opencode');
|
|
38
|
+
track(prepared.ws);
|
|
39
|
+
assertTopology(prepared.ws);
|
|
40
|
+
assert.strictEqual(prepared.fixture_setup.status, 'pass');
|
|
41
|
+
assert.match(prepared.fixture_setup.script_sha256, /^[0-9a-f]{64}$/);
|
|
42
|
+
topologyFingerprints.push(topologyFingerprint(prepared.ws));
|
|
43
|
+
setupHashes.push(prepared.fixture_setup.script_sha256);
|
|
44
|
+
const failed = paired.prepareArmWorkspace(failure, effect, arm, 'opencode');
|
|
45
|
+
track(failed.ws);
|
|
46
|
+
assert.strictEqual(failed.ok, false);
|
|
47
|
+
assert.strictEqual(failed.outcome, 'fixture-error');
|
|
48
|
+
assert.strictEqual(failed.fixture_setup.exit_code, 17);
|
|
49
|
+
assert.ok(!fs.existsSync(path.join(failed.ws, '.claude')), 'setup failure must precede skill installation');
|
|
50
|
+
assert.ok(!fs.existsSync(path.join(failed.ws, '.opencode')), 'setup failure must precede plugin installation');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
assert.ok(topologyFingerprints.length >= 4);
|
|
54
|
+
for (const fingerprint of topologyFingerprints.slice(1)) {
|
|
55
|
+
assert.deepStrictEqual(fingerprint, topologyFingerprints[0], 'fixture setup must produce identical Git object identities across independent arms/runs');
|
|
56
|
+
}
|
|
57
|
+
assert.strictEqual(new Set(setupHashes).size, 1, 'all arms must retain the same checked-in setup script hash');
|
|
58
|
+
|
|
59
|
+
const pairDir = track(fs.mkdtempSync(path.join(os.tmpdir(), 'fixture-entrypoints-')));
|
|
60
|
+
let launches = 0;
|
|
61
|
+
const context = { effect_type: 'skill-text', engine: 'claude', model: 'test' };
|
|
62
|
+
const arms = {};
|
|
63
|
+
for (const arm of ['baseline', 'treatment']) {
|
|
64
|
+
arms[arm] = paired.runArm(failure, context, arm, pairDir, () => { launches++; throw new Error('model must not start'); });
|
|
65
|
+
assert.strictEqual(arms[arm].outcome, 'fixture-error');
|
|
66
|
+
assert.strictEqual(arms[arm].fixture_setup.exit_code, 17);
|
|
67
|
+
assert.strictEqual(arms[arm].tool_call_count, null);
|
|
68
|
+
}
|
|
69
|
+
assert.strictEqual(launches, 0);
|
|
70
|
+
const summary = paired.summarizePairs([{ id: topology.id, arms }], 10);
|
|
71
|
+
assert.strictEqual(summary.fixture_failures, 1);
|
|
72
|
+
assert.strictEqual(summary.included_pairs, 0);
|
|
73
|
+
assert.deepStrictEqual(summary.arm_outcomes, { baseline: { pass: 0, fail: 0 }, treatment: { pass: 0, fail: 0 } });
|
|
74
|
+
|
|
75
|
+
const plugin = require('../behavioral-evals/run-with-plugin');
|
|
76
|
+
const failed = plugin.runCase(failure, () => { launches++; throw new Error('model must not start'); });
|
|
77
|
+
track(failed.workspace);
|
|
78
|
+
assert.strictEqual(failed.outcome, 'fixture-error');
|
|
79
|
+
assert.strictEqual(failed.fixture_setup.exit_code, 17);
|
|
80
|
+
assert.strictEqual(launches, 0);
|
|
81
|
+
assert.ok(!fs.existsSync(path.join(failed.workspace, '.harness-src')), 'failed setup must precede Harness installation');
|
|
82
|
+
|
|
83
|
+
// Exercise the same public case path as the plugin CLI, without calling a model.
|
|
84
|
+
const result = plugin.runCase(topology, (_prompt, ws) => {
|
|
85
|
+
launches++;
|
|
86
|
+
assertTopology(ws);
|
|
87
|
+
const transcript = path.join(pairDir, 'plugin-transcript.jsonl');
|
|
88
|
+
fs.writeFileSync(transcript, '');
|
|
89
|
+
return transcript;
|
|
90
|
+
});
|
|
91
|
+
track(result.workspace);
|
|
92
|
+
assert.strictEqual(launches, 1);
|
|
93
|
+
assert.strictEqual(result.fixture_setup.status, 'pass');
|
|
94
|
+
assert.strictEqual(result.outcome, 'fail', 'an untouched topology must fail the actual final-state graders');
|
|
95
|
+
|
|
96
|
+
for (const fixture of [
|
|
97
|
+
{ files: [{ path: '../escape', content: 'bad' }] },
|
|
98
|
+
{ files: [{ path: 'seed', content: 'ok' }], setup: '../escape.js' },
|
|
99
|
+
]) {
|
|
100
|
+
const result = prepareWorkspace({ id: 'invalid-fixture', fixture });
|
|
101
|
+
track(result.workspace);
|
|
102
|
+
assert.strictEqual(result.outcome, 'fixture-error');
|
|
103
|
+
}
|
|
104
|
+
// The public CLIs must retain fixture-error evidence and exit nonzero.
|
|
105
|
+
const caseId = `fixture-entrypoints-failure-${process.pid}`;
|
|
106
|
+
const casePath = path.join(ROOT, 'behavioral-evals/cases', `${caseId}.yaml`);
|
|
107
|
+
fs.writeFileSync(casePath, `id: ${caseId}
|
|
108
|
+
name: Fixture failure CLI control
|
|
109
|
+
loaded_skills:
|
|
110
|
+
- git-commit
|
|
111
|
+
fixture:
|
|
112
|
+
files:
|
|
113
|
+
- path: seed.txt
|
|
114
|
+
content: seed
|
|
115
|
+
setup: behavioral-evals/fixtures/setup/fail.js
|
|
116
|
+
prompt: This model session must never start.
|
|
117
|
+
max_turns: 1
|
|
118
|
+
expectations:
|
|
119
|
+
- type: file_contains
|
|
120
|
+
path: seed.txt
|
|
121
|
+
value: seed
|
|
122
|
+
`);
|
|
123
|
+
paths.push(casePath);
|
|
124
|
+
const pairedCli = spawnSync(process.execPath, [
|
|
125
|
+
path.join(ROOT, 'behavioral-evals/paired-benchmark.js'), 'run',
|
|
126
|
+
'--effect', 'skill-text', '--engine', 'claude', '--model', 'fixture-test',
|
|
127
|
+
'--min-effect-pp', '10', '--case', caseId,
|
|
128
|
+
], { cwd: ROOT, encoding: 'utf8', timeout: 30000 });
|
|
129
|
+
const summaryMatch = pairedCli.stdout.match(/Summary: (.+)/);
|
|
130
|
+
assert.ok(summaryMatch, pairedCli.stderr || pairedCli.stdout);
|
|
131
|
+
const summaryPath = summaryMatch[1].trim();
|
|
132
|
+
track(path.dirname(summaryPath));
|
|
133
|
+
assert.strictEqual(pairedCli.status, 1, pairedCli.stderr);
|
|
134
|
+
const cliSummary = JSON.parse(fs.readFileSync(summaryPath, 'utf8'));
|
|
135
|
+
assert.strictEqual(cliSummary.fixture_failures, 1);
|
|
136
|
+
assert.strictEqual(cliSummary.included_pairs, 0);
|
|
137
|
+
const pluginCli = spawnSync(process.execPath, [
|
|
138
|
+
path.join(ROOT, 'behavioral-evals/run-with-plugin.js'), 'run', '--case', caseId,
|
|
139
|
+
], { cwd: ROOT, encoding: 'utf8', timeout: 30000 });
|
|
140
|
+
assert.strictEqual(pluginCli.status, 1, pluginCli.stderr);
|
|
141
|
+
const resultMatch = pluginCli.stdout.match(/fixture-error -> (.+)/);
|
|
142
|
+
assert.ok(resultMatch, pluginCli.stdout);
|
|
143
|
+
const resultPath = resultMatch[1].trim();
|
|
144
|
+
paths.push(resultPath);
|
|
145
|
+
const pluginFailure = JSON.parse(fs.readFileSync(resultPath, 'utf8'));
|
|
146
|
+
track(pluginFailure.workspace);
|
|
147
|
+
assert.strictEqual(pluginFailure.fixture_setup.exit_code, 17);
|
|
148
|
+
assert.ok(!fs.existsSync(path.join(pluginFailure.workspace, '.harness-src')));
|
|
149
|
+
|
|
150
|
+
console.log('Fixture entrypoints: paired baseline/treatment and plugin execute setup; failures skip installation/model and are excluded from scores.');
|
|
151
|
+
} finally {
|
|
152
|
+
for (const dir of paths) fs.rmSync(dir, { recursive: true, force: true });
|
|
153
|
+
}
|
|
@@ -21,6 +21,32 @@ assert.deepStrictEqual(validateCase({
|
|
|
21
21
|
expectations: [{ type: 'tool_call', value: 'Bash' }]
|
|
22
22
|
}), [], 'tool_call must remain part of the runner/validator schema');
|
|
23
23
|
|
|
24
|
+
const setupCase = setup => ({
|
|
25
|
+
id: 'fixture-setup-schema',
|
|
26
|
+
prompt: 'inspect the prepared fixture',
|
|
27
|
+
max_turns: 1,
|
|
28
|
+
fixture: {
|
|
29
|
+
files: [{ path: 'README.md', content: 'fixture' }],
|
|
30
|
+
setup,
|
|
31
|
+
},
|
|
32
|
+
expectations: [{ type: 'trace_contains', value: 'fixture' }],
|
|
33
|
+
});
|
|
34
|
+
assert.deepStrictEqual(
|
|
35
|
+
validateCase(setupCase('behavioral-evals/fixtures/setup/noop.js')),
|
|
36
|
+
[],
|
|
37
|
+
'checked-in repository-relative .js setup should be valid'
|
|
38
|
+
);
|
|
39
|
+
for (const [setup, expected] of [
|
|
40
|
+
['../escape.js', /fixture\.setup/],
|
|
41
|
+
['/tmp/escape.js', /fixture\.setup/],
|
|
42
|
+
['behavioral-evals/fixtures/setup/noop.txt', /\.js/],
|
|
43
|
+
['behavioral-evals/fixtures/setup/missing.js', /missing|checked-in/],
|
|
44
|
+
[{ command: 'echo unsafe' }, /fixture\.setup/],
|
|
45
|
+
]) {
|
|
46
|
+
const setupErrors = validateCase(setupCase(setup));
|
|
47
|
+
assert.ok(setupErrors.some(error => expected.test(error)), `setup ${JSON.stringify(setup)} should be rejected: ${setupErrors.join('; ')}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
24
50
|
const transcriptDir = fs.mkdtempSync(path.join(os.tmpdir(), 'behavioral-case-validation-'));
|
|
25
51
|
try {
|
|
26
52
|
const transcriptPath = path.join(transcriptDir, 'trace.jsonl');
|
package/package.json
CHANGED
|
@@ -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.
|
|
4
|
+
"version": "0.28.0",
|
|
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",
|
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:a9471ff7-cd1e-58b8-b661-a6231462bdd7",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-09-27T17:
|
|
8
|
+
"timestamp": "2026-09-27T17:31:25.591Z",
|
|
9
9
|
"tools": [
|
|
10
10
|
{
|
|
11
11
|
"vendor": "Harness Everything",
|
|
12
12
|
"name": "generate-sbom.js",
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.28.0"
|
|
14
14
|
}
|
|
15
15
|
],
|
|
16
16
|
"component": {
|
|
17
17
|
"type": "application",
|
|
18
|
-
"bom-ref": "pkg:npm/harness-everything@0.
|
|
18
|
+
"bom-ref": "pkg:npm/harness-everything@0.28.0",
|
|
19
19
|
"name": "harness-everything",
|
|
20
|
-
"version": "0.
|
|
21
|
-
"purl": "pkg:npm/harness-everything@0.
|
|
20
|
+
"version": "0.28.0",
|
|
21
|
+
"purl": "pkg:npm/harness-everything@0.28.0"
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
24
|
"components": [
|
|
@@ -1999,7 +1999,7 @@
|
|
|
1999
1999
|
]
|
|
2000
2000
|
},
|
|
2001
2001
|
{
|
|
2002
|
-
"ref": "pkg:npm/harness-everything@0.
|
|
2002
|
+
"ref": "pkg:npm/harness-everything@0.28.0",
|
|
2003
2003
|
"dependsOn": [
|
|
2004
2004
|
"pkg:npm/%40commitlint/cli@19.8.1",
|
|
2005
2005
|
"pkg:npm/%40commitlint/config-conventional@19.8.1",
|