harness-everything 0.27.0 → 0.29.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 +33 -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/benchmarks/fixtures/system-one-holdout-reviews.json +18 -18
- package/benchmarks/fixtures/system-one-holdout.json +9 -9
- package/benchmarks/results/system-one/harness-routing-v1-cpu-2026-09-24/README.md +75 -0
- package/benchmarks/results/system-one/harness-routing-v1-cpu-2026-09-24/report.json +10832 -0
- package/benchmarks/results/system-one/harness-routing-v1-ngram-2026-09-24/README.md +72 -0
- package/benchmarks/results/system-one/harness-routing-v1-ngram-2026-09-24/report.json +10817 -0
- package/benchmarks/results/system-one/harness-routing-v3-ngram-2026-09-24/README.md +63 -0
- package/benchmarks/results/system-one/harness-routing-v3-ngram-2026-09-24/report.json +10817 -0
- package/benchmarks/results/system-one/labeler-holdout-check-2026-09-24/report.json +1104 -0
- package/benchmarks/results/system-one/labeler-holdout-check-codex-2026-09-24/report.json +1104 -0
- package/benchmarks/results/system-one/labeler-holdout-check-codex-tier3-2026-09-24/report.json +1104 -0
- package/ci/mechanism-2j-behavioral-runner.test.js +121 -1
- package/ci/mechanism-2j-fixture-entrypoints.test.js +153 -0
- package/ci/mechanism-system-one-evaluation.test.js +17 -0
- package/ci/mechanism-system-one-label.test.js +27 -0
- package/ci/mechanism-system-one-ngram.test.js +161 -0
- package/ci/mechanism-system-one-train.test.js +12 -0
- package/ci/system-one-ngram-parity.py +19 -0
- package/ci/system-one-train-test.py +6 -0
- package/ci/test-behavioral-case-validation.js +26 -0
- package/docs/system-one-corpus.md +31 -3
- package/docs/system-one-routing.md +84 -2
- package/docs/system-one-training.md +14 -2
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/system-one/evaluate.js +3 -2
- package/harness-everything/scripts/system-one/ngram.js +84 -0
- package/harness-everything/scripts/system-one/provider.js +15 -5
- 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/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/evaluate.js +3 -2
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/ngram.js +84 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/system-one/provider.js +15 -5
- package/sbom.cdx.json +7 -7
- package/scripts/evaluate-system-one.js +16 -9
- package/scripts/system-one-calibrate.js +1 -1
- package/scripts/system-one-label.js +32 -3
- package/scripts/system-one-train-ngram.py +129 -0
- package/scripts/system-one-train.py +14 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "harness-everything",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.29.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.29.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Miya Daniel"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.29.0](https://github.com/dyphn1/Harness-everything/compare/v0.28.0...v0.29.0) (2026-09-28)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* **router:** advisory 85% system one gate and class-balanced training ([15fbfbe](https://github.com/dyphn1/Harness-everything/commit/15fbfbe9b2789a8f148e088629373ac7902e9328)), closes [#233](https://github.com/dyphn1/Harness-everything/issues/233)
|
|
10
|
+
* **router:** label system one prompts with stateless codex calls ([1791fa3](https://github.com/dyphn1/Harness-everything/commit/1791fa35a2de5706d6c7b35b1ac5ec11399d892f)), closes [#233](https://github.com/dyphn1/Harness-everything/issues/233)
|
|
11
|
+
* **router:** score system one tiers in process with an ngram model ([a424e71](https://github.com/dyphn1/Harness-everything/commit/a424e71868121bd7f19a27f5ab560ad3d52d5617)), closes [#233](https://github.com/dyphn1/Harness-everything/issues/233)
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **router:** evaluate the ngram transport by its own latency and provenance ([ad85111](https://github.com/dyphn1/Harness-everything/commit/ad85111a4631d648269066accb1b517de5002b1c)), closes [#233](https://github.com/dyphn1/Harness-everything/issues/233)
|
|
16
|
+
|
|
17
|
+
## [0.28.0](https://github.com/dyphn1/Harness-everything/compare/v0.27.0...v0.28.0) (2026-09-27)
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* **behavioral-evals:** bind paired evidence to setup script ([488e03a](https://github.com/dyphn1/Harness-everything/commit/488e03ac0a019c072e732be0262a3b8b49de1ea7))
|
|
22
|
+
* **behavioral-evals:** retain fixture setup content hash ([fc6670a](https://github.com/dyphn1/Harness-everything/commit/fc6670ae6257dad5706a3f94be479de91b270f02))
|
|
23
|
+
* **behavioral-evals:** run validated fixture setup before model sessions ([177b698](https://github.com/dyphn1/Harness-everything/commit/177b698ab606781d7d8f6d126949c22be986b7c7))
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* **behavioral-evals:** fail runs on fixture setup errors ([49259ff](https://github.com/dyphn1/Harness-everything/commit/49259ff6aa4fefa4f4b70e03af7beded53e84295))
|
|
28
|
+
* **behavioral-evals:** keep informational label in plugin runner output ([32b9556](https://github.com/dyphn1/Harness-everything/commit/32b95566ba406ebc3b6792a817d12eeaff89867f))
|
|
29
|
+
* **behavioral-evals:** make worktree fixture reproducible ([4c1b627](https://github.com/dyphn1/Harness-everything/commit/4c1b6276a1f87840b11a07c2cdca324e9743fadd))
|
|
30
|
+
* **behavioral-evals:** prepare fixtures before every model entrypoint ([a8be8be](https://github.com/dyphn1/Harness-everything/commit/a8be8be0268b9e351840a35d7ac85952e77888d5))
|
|
31
|
+
* **skills:** parse clean submodule reachability status ([e720af1](https://github.com/dyphn1/Harness-everything/commit/e720af12e90981df018b9c32a4749d69f743f3e7))
|
|
32
|
+
* **stack:** sync PR 279 mechanism-34-worktree-submodule-reachability.test.js ([a94f904](https://github.com/dyphn1/Harness-everything/commit/a94f90480b37f7e59098f7bf624b347d1e5c41fc))
|
|
33
|
+
* **stack:** sync PR 279 submodule-reachability.js ([4d38ae0](https://github.com/dyphn1/Harness-everything/commit/4d38ae0f253e07e6cd48073c61d72ba8fc27d84c))
|
|
34
|
+
* **stack:** sync PR 279 submodule-reachability.js ([7b85d6d](https://github.com/dyphn1/Harness-everything/commit/7b85d6d329e1190f8f8649bdb2a2c289fc61428b))
|
|
35
|
+
* **stack:** sync PR 279 SUBMODULES.md ([0b1667f](https://github.com/dyphn1/Harness-everything/commit/0b1667fa71edc6995e7dfd3199a35b0f95c595be))
|
|
36
|
+
* **stack:** sync PR 279 SUBMODULES.md ([9540ce5](https://github.com/dyphn1/Harness-everything/commit/9540ce5adc2c8b15cb3937a9dc8a6d8b4e556344))
|
|
37
|
+
|
|
5
38
|
## [0.27.0](https://github.com/dyphn1/Harness-everything/compare/v0.26.0...v0.27.0) (2026-09-27)
|
|
6
39
|
|
|
7
40
|
### 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 };
|