harness-everything 0.5.2 → 0.6.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/AGENTS.md +5 -1
- package/BENCHMARK_SOP.md +1 -1
- package/CHANGELOG.md +12 -0
- package/README.md +3 -3
- package/VERIFICATION.md +5 -5
- package/behavioral-evals/PAIRED-BENCHMARK.md +186 -0
- package/behavioral-evals/README.md +46 -0
- package/behavioral-evals/opencode-hardlock-live.js +301 -0
- package/behavioral-evals/paired-benchmark.js +781 -0
- package/behavioral-evals/run-with-plugin.js +1 -1
- package/benchmarks/results/live-host/opencode-2026-09-16/README.md +133 -0
- package/benchmarks/results/live-host/opencode-2026-09-16/probe-control.md +88 -0
- package/benchmarks/results/live-host/opencode-2026-09-16/run-commands.md +100 -0
- package/benchmarks/results/live-host/opencode-2026-09-16/state/sessions/ses_f5520f1d9ffeB8KLTIUKgA5ng8/circuit-breaker.json +16 -0
- package/benchmarks/results/live-host/opencode-2026-09-16/state/sessions/ses_f5520f1d9ffeB8KLTIUKgA5ng8/compliance.json +7 -0
- package/benchmarks/results/live-host/opencode-2026-09-16/state/sessions/ses_f5520f1d9ffeB8KLTIUKgA5ng8/edit-state.json +10 -0
- package/benchmarks/results/live-host/opencode-2026-09-16/state/sessions/ses_f5520f1d9ffeB8KLTIUKgA5ng8/zoom-out-report.md +29 -0
- package/ci/doc-capability-consistency.test.js +56 -5
- package/ci/mechanism-2w-platform-compatibility.test.js +20 -7
- package/ci/mechanism-30-opencode-plugin-loadability.test.js +86 -0
- package/ci/mechanism-31-opencode-hardlock-evidence.test.js +138 -0
- package/ci/mechanism-32-paired-benchmark.test.js +254 -0
- package/docs/architecture.md +2 -2
- package/docs/platform-capabilities.md +3 -3
- package/docs/platform-compatibility.json +4 -4
- package/opencode-plugin/README.md +32 -11
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +5 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const helper = require('./test-helper');
|
|
4
|
+
const {
|
|
5
|
+
BLOCKED_MARKER,
|
|
6
|
+
HARDLOCK_MESSAGE,
|
|
7
|
+
INSTALLED_PLUGIN_NAME,
|
|
8
|
+
POST_REFLECTION_MARKER,
|
|
9
|
+
PRELOCK_MARKER,
|
|
10
|
+
verifyEvidence,
|
|
11
|
+
} = require('../behavioral-evals/opencode-hardlock-live');
|
|
12
|
+
|
|
13
|
+
console.log('\n[31] opencode live hard-lock evidence contract...');
|
|
14
|
+
|
|
15
|
+
function writeJson(file, value) {
|
|
16
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
17
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function makeEvidence() {
|
|
21
|
+
const dir = helper.tempDir('.mechanism-test-opencode-live-evidence');
|
|
22
|
+
fs.mkdirSync(path.join(dir, 'workspace'), { recursive: true });
|
|
23
|
+
fs.mkdirSync(path.join(dir, 'state'), { recursive: true });
|
|
24
|
+
const pluginHash = 'a'.repeat(64);
|
|
25
|
+
writeJson(path.join(dir, 'metadata.json'), {
|
|
26
|
+
schemaVersion: 1,
|
|
27
|
+
stateIsolated: true,
|
|
28
|
+
installedPluginName: INSTALLED_PLUGIN_NAME,
|
|
29
|
+
pluginSha256: pluginHash,
|
|
30
|
+
installedPluginSha256: pluginHash,
|
|
31
|
+
});
|
|
32
|
+
const toolEvent = (input) => JSON.stringify({
|
|
33
|
+
type: 'tool',
|
|
34
|
+
part: { tool: 'edit', state: { input } },
|
|
35
|
+
});
|
|
36
|
+
fs.writeFileSync(path.join(dir, 'transcript.jsonl'), [
|
|
37
|
+
toolEvent({ filePath: 'probe.txt', text: PRELOCK_MARKER }),
|
|
38
|
+
toolEvent({ filePath: 'probe.txt', text: POST_REFLECTION_MARKER }),
|
|
39
|
+
toolEvent({ filePath: 'probe.txt', text: BLOCKED_MARKER }),
|
|
40
|
+
JSON.stringify({ type: 'text', part: { text: `${HARDLOCK_MESSAGE} after a repeat failure post-reflection.` } }),
|
|
41
|
+
'',
|
|
42
|
+
].join('\n'));
|
|
43
|
+
fs.writeFileSync(path.join(dir, 'stderr.txt'), '', 'utf8');
|
|
44
|
+
fs.writeFileSync(path.join(dir, 'workspace', 'probe.txt'), `baseline\n${PRELOCK_MARKER}\n${POST_REFLECTION_MARKER}\n`, 'utf8');
|
|
45
|
+
writeJson(path.join(dir, 'state', 'circuit-breaker.json'), {
|
|
46
|
+
failures: {
|
|
47
|
+
'npm test: HARNESS_LIVE_PROBE_RED': { count: 4, firstSeen: 1, lastSeen: 5 },
|
|
48
|
+
},
|
|
49
|
+
hardLock: true,
|
|
50
|
+
lastReflection: 4,
|
|
51
|
+
lastReflectionSignature: 'npm test: HARNESS_LIVE_PROBE_RED',
|
|
52
|
+
reflectionPending: false,
|
|
53
|
+
reflectionRequestedAt: null,
|
|
54
|
+
reflectionToken: null,
|
|
55
|
+
reflectionSignature: null,
|
|
56
|
+
});
|
|
57
|
+
fs.writeFileSync(path.join(dir, 'state', 'zoom-out-report.md'), [
|
|
58
|
+
'## Goal',
|
|
59
|
+
'exercise the breaker',
|
|
60
|
+
'## Failed Attempts',
|
|
61
|
+
'three controlled failures',
|
|
62
|
+
'## Verified Facts',
|
|
63
|
+
'the verification fixture intentionally stays red',
|
|
64
|
+
'## Diagnosis',
|
|
65
|
+
'this is a live enforcement probe',
|
|
66
|
+
'## Decision',
|
|
67
|
+
'RESUME: reproduce the same failure once after reflection',
|
|
68
|
+
'Reflection token: abc123',
|
|
69
|
+
'',
|
|
70
|
+
].join('\n'));
|
|
71
|
+
return dir;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const valid = makeEvidence();
|
|
76
|
+
let result = verifyEvidence(valid);
|
|
77
|
+
helper.check('31. complete attributed hard-lock evidence passes', result.passed, JSON.stringify(result.checks.filter((entry) => !entry.pass)));
|
|
78
|
+
|
|
79
|
+
const wrongPlugin = makeEvidence();
|
|
80
|
+
const metadataFile = path.join(wrongPlugin, 'metadata.json');
|
|
81
|
+
const metadata = JSON.parse(fs.readFileSync(metadataFile, 'utf8'));
|
|
82
|
+
metadata.installedPluginSha256 = 'b'.repeat(64);
|
|
83
|
+
writeJson(metadataFile, metadata);
|
|
84
|
+
result = verifyEvidence(wrongPlugin);
|
|
85
|
+
helper.check(
|
|
86
|
+
'31. evidence fails when the installed plugin hash differs from the canonical source hash',
|
|
87
|
+
!result.passed && result.checks.some((entry) => entry.name === 'installed plugin is byte-identical to the recorded canonical source' && !entry.pass),
|
|
88
|
+
JSON.stringify(result.checks),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const landed = makeEvidence();
|
|
92
|
+
fs.appendFileSync(path.join(landed, 'workspace', 'probe.txt'), `${BLOCKED_MARKER}\n`);
|
|
93
|
+
result = verifyEvidence(landed);
|
|
94
|
+
helper.check(
|
|
95
|
+
'31. negative control fails if the blocked marker reached the filesystem',
|
|
96
|
+
!result.passed && result.checks.some((entry) => entry.name === 'blocked marker did not reach the filesystem' && !entry.pass),
|
|
97
|
+
JSON.stringify(result.checks),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const unlocked = makeEvidence();
|
|
101
|
+
const breakerFile = path.join(unlocked, 'state', 'circuit-breaker.json');
|
|
102
|
+
const breaker = JSON.parse(fs.readFileSync(breakerFile, 'utf8'));
|
|
103
|
+
breaker.hardLock = false;
|
|
104
|
+
writeJson(breakerFile, breaker);
|
|
105
|
+
result = verifyEvidence(unlocked);
|
|
106
|
+
helper.check(
|
|
107
|
+
'31. evidence fails without a pre-reset hard-lock snapshot',
|
|
108
|
+
!result.passed && result.checks.some((entry) => entry.name === 'pre-reset breaker snapshot is hard-locked' && !entry.pass),
|
|
109
|
+
JSON.stringify(result.checks),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const noAttempt = makeEvidence();
|
|
113
|
+
const transcript = fs.readFileSync(path.join(noAttempt, 'transcript.jsonl'), 'utf8')
|
|
114
|
+
.split(/\r?\n/)
|
|
115
|
+
.filter((line) => !line.includes(BLOCKED_MARKER))
|
|
116
|
+
.join('\n');
|
|
117
|
+
fs.writeFileSync(path.join(noAttempt, 'transcript.jsonl'), transcript);
|
|
118
|
+
result = verifyEvidence(noAttempt);
|
|
119
|
+
helper.check(
|
|
120
|
+
'31. evidence fails when the final blocked edit was never observed as a structured tool attempt',
|
|
121
|
+
!result.passed && result.checks.some((entry) => entry.name === 'structured tool trace contains the final blocked edit attempt' && !entry.pass),
|
|
122
|
+
JSON.stringify(result.checks),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const noReflection = makeEvidence();
|
|
126
|
+
fs.rmSync(path.join(noReflection, 'state', 'zoom-out-report.md'));
|
|
127
|
+
result = verifyEvidence(noReflection);
|
|
128
|
+
helper.check(
|
|
129
|
+
'31. evidence fails without the preserved reflection artifact',
|
|
130
|
+
!result.passed && result.checks.some((entry) => entry.name === 'reflection artifact is valid and preserved' && !entry.pass),
|
|
131
|
+
JSON.stringify(result.checks),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
helper.finish();
|
|
135
|
+
} catch (error) {
|
|
136
|
+
helper.check('31. OpenCode hard-lock live evidence contract', false, error.stack);
|
|
137
|
+
helper.finish();
|
|
138
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const helper = require('./test-helper');
|
|
7
|
+
const {
|
|
8
|
+
mcnemarExactP,
|
|
9
|
+
prepareArmWorkspace,
|
|
10
|
+
summarizePairs,
|
|
11
|
+
validatePairRecord,
|
|
12
|
+
validateRunConfig,
|
|
13
|
+
} = require('../behavioral-evals/paired-benchmark');
|
|
14
|
+
|
|
15
|
+
console.log('\n[32] paired behavioral benchmark contract...');
|
|
16
|
+
|
|
17
|
+
function stable(value) {
|
|
18
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
19
|
+
if (!value || typeof value !== 'object') return value;
|
|
20
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])]));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hash(value) {
|
|
24
|
+
return crypto.createHash('sha256').update(JSON.stringify(stable(value))).digest('hex');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function skillContract(caseId = 'case-a') {
|
|
28
|
+
return {
|
|
29
|
+
schema_version: 1,
|
|
30
|
+
effect_type: 'skill-text',
|
|
31
|
+
engine: 'opencode',
|
|
32
|
+
requested_model: 'test/model',
|
|
33
|
+
case_id: caseId,
|
|
34
|
+
max_turns: 8,
|
|
35
|
+
loaded_skills: ['tdd'],
|
|
36
|
+
fixture_sha256: 'fixture-hash',
|
|
37
|
+
prompt_sha256: 'prompt-hash',
|
|
38
|
+
rubric_sha256: 'rubric-hash',
|
|
39
|
+
execution_policy: 'opencode-auto',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function arm(outcome, treatment, overrides = {}) {
|
|
44
|
+
return {
|
|
45
|
+
outcome,
|
|
46
|
+
model_name: 'test/model',
|
|
47
|
+
intervention: {
|
|
48
|
+
skill_text: treatment,
|
|
49
|
+
plugin_enforcement: false,
|
|
50
|
+
},
|
|
51
|
+
loaded_skills: treatment ? ['tdd'] : [],
|
|
52
|
+
plugin_sha256: null,
|
|
53
|
+
plugin_attribution: null,
|
|
54
|
+
cost: treatment ? 2 : 1,
|
|
55
|
+
total_tokens: treatment ? 120 : 100,
|
|
56
|
+
tool_call_count: treatment ? 4 : 3,
|
|
57
|
+
duration_ms: treatment ? 1200 : 1000,
|
|
58
|
+
...overrides,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function skillPair(id, baselineOutcome, treatmentOutcome, overrides = {}) {
|
|
63
|
+
const contract = skillContract(overrides.caseId || id);
|
|
64
|
+
return {
|
|
65
|
+
schema_version: 1,
|
|
66
|
+
id: overrides.caseId || id,
|
|
67
|
+
pair_id: id,
|
|
68
|
+
effect_type: 'skill-text',
|
|
69
|
+
contract,
|
|
70
|
+
contract_sha256: hash(contract),
|
|
71
|
+
arms: {
|
|
72
|
+
baseline: arm(baselineOutcome, false, overrides.baseline || {}),
|
|
73
|
+
treatment: arm(treatmentOutcome, true, overrides.treatment || {}),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const missingThreshold = validateRunConfig({
|
|
80
|
+
effect_type: 'skill-text', engine: 'opencode', model: 'test/model',
|
|
81
|
+
min_effect_pp: NaN, repeats: 1,
|
|
82
|
+
});
|
|
83
|
+
helper.check(
|
|
84
|
+
'32. a paired study cannot start without a predeclared minimum effect',
|
|
85
|
+
missingThreshold.some((problem) => problem.includes('min-effect-pp')),
|
|
86
|
+
missingThreshold.join('; '),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const noModel = validateRunConfig({
|
|
90
|
+
effect_type: 'skill-text', engine: 'opencode', model: '',
|
|
91
|
+
min_effect_pp: 10, repeats: 1,
|
|
92
|
+
});
|
|
93
|
+
helper.check(
|
|
94
|
+
'32. an explicit model is required so both arms share the same model contract',
|
|
95
|
+
noModel.some((problem) => problem.includes('explicit --model')),
|
|
96
|
+
noModel.join('; '),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const badPluginEngine = validateRunConfig({
|
|
100
|
+
effect_type: 'plugin-enforcement', engine: 'claude', model: 'test/model',
|
|
101
|
+
min_effect_pp: 10, repeats: 1,
|
|
102
|
+
});
|
|
103
|
+
helper.check(
|
|
104
|
+
'32. plugin-enforcement is not generalized beyond the currently attributed OpenCode surface',
|
|
105
|
+
badPluginEngine.some((problem) => problem.includes('opencode only')),
|
|
106
|
+
badPluginEngine.join('; '),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const fixtureCase = {
|
|
110
|
+
id: 'workspace-shape',
|
|
111
|
+
discipline: 'tdd',
|
|
112
|
+
fixture: { files: [{ path: 'probe.txt', content: 'baseline\n' }] },
|
|
113
|
+
};
|
|
114
|
+
const skillBaseline = prepareArmWorkspace(fixtureCase, 'skill-text', 'baseline', 'opencode');
|
|
115
|
+
const skillTreatment = prepareArmWorkspace(fixtureCase, 'skill-text', 'treatment', 'opencode');
|
|
116
|
+
helper.check(
|
|
117
|
+
'32. skill-text control contains no Harness skill or plugin',
|
|
118
|
+
!fs.existsSync(path.join(skillBaseline.ws, '.claude', 'skills', 'tdd', 'SKILL.md')) &&
|
|
119
|
+
!fs.existsSync(path.join(skillBaseline.ws, '.opencode', 'plugins', 'harness-enforcement.js')),
|
|
120
|
+
skillBaseline.ws,
|
|
121
|
+
);
|
|
122
|
+
helper.check(
|
|
123
|
+
'32. skill-text treatment installs the named skill without installing hooks/plugin enforcement',
|
|
124
|
+
fs.existsSync(path.join(skillTreatment.ws, '.claude', 'skills', 'tdd', 'SKILL.md')) &&
|
|
125
|
+
!fs.existsSync(path.join(skillTreatment.ws, '.claude', 'settings.json')) &&
|
|
126
|
+
!fs.existsSync(path.join(skillTreatment.ws, '.opencode', 'plugins', 'harness-enforcement.js')),
|
|
127
|
+
skillTreatment.ws,
|
|
128
|
+
);
|
|
129
|
+
fs.rmSync(skillBaseline.ws, { recursive: true, force: true });
|
|
130
|
+
fs.rmSync(skillTreatment.ws, { recursive: true, force: true });
|
|
131
|
+
|
|
132
|
+
const pluginBaseline = prepareArmWorkspace(fixtureCase, 'plugin-enforcement', 'baseline', 'opencode');
|
|
133
|
+
const pluginTreatment = prepareArmWorkspace(fixtureCase, 'plugin-enforcement', 'treatment', 'opencode');
|
|
134
|
+
helper.check(
|
|
135
|
+
'32. plugin experiment holds skill text constant and changes only enforcement installation',
|
|
136
|
+
fs.existsSync(path.join(pluginBaseline.ws, '.claude', 'skills', 'tdd', 'SKILL.md')) &&
|
|
137
|
+
fs.existsSync(path.join(pluginTreatment.ws, '.claude', 'skills', 'tdd', 'SKILL.md')) &&
|
|
138
|
+
!fs.existsSync(path.join(pluginBaseline.ws, '.opencode', 'plugins', 'harness-enforcement.js')) &&
|
|
139
|
+
fs.existsSync(path.join(pluginTreatment.ws, '.opencode', 'plugins', 'harness-enforcement.js')),
|
|
140
|
+
JSON.stringify({ baseline: pluginBaseline.ws, treatment: pluginTreatment.ws }),
|
|
141
|
+
);
|
|
142
|
+
fs.rmSync(pluginBaseline.ws, { recursive: true, force: true });
|
|
143
|
+
fs.rmSync(pluginTreatment.ws, { recursive: true, force: true });
|
|
144
|
+
|
|
145
|
+
const validSkillPair = skillPair('p1', 'fail', 'pass');
|
|
146
|
+
helper.check('32. a correctly isolated skill-text pair is includable', validatePairRecord(validSkillPair).included, JSON.stringify(validatePairRecord(validSkillPair)));
|
|
147
|
+
|
|
148
|
+
const modelMismatch = skillPair('model-mismatch', 'fail', 'pass', {
|
|
149
|
+
treatment: { model_name: 'other/model' },
|
|
150
|
+
});
|
|
151
|
+
helper.check(
|
|
152
|
+
'32. actual model mismatch excludes the pair instead of counting it as behavior',
|
|
153
|
+
!validatePairRecord(modelMismatch).included && validatePairRecord(modelMismatch).reason === 'actual-model-mismatch',
|
|
154
|
+
JSON.stringify(validatePairRecord(modelMismatch)),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const pluginContract = {
|
|
158
|
+
schema_version: 1,
|
|
159
|
+
effect_type: 'plugin-enforcement',
|
|
160
|
+
engine: 'opencode',
|
|
161
|
+
requested_model: 'test/model',
|
|
162
|
+
case_id: 'plugin-case',
|
|
163
|
+
max_turns: 8,
|
|
164
|
+
loaded_skills: ['tdd'],
|
|
165
|
+
fixture_sha256: 'fixture-hash',
|
|
166
|
+
prompt_sha256: 'prompt-hash',
|
|
167
|
+
rubric_sha256: 'rubric-hash',
|
|
168
|
+
execution_policy: 'opencode-auto',
|
|
169
|
+
};
|
|
170
|
+
const pluginPair = {
|
|
171
|
+
id: 'plugin-case',
|
|
172
|
+
pair_id: 'plugin-pair',
|
|
173
|
+
effect_type: 'plugin-enforcement',
|
|
174
|
+
contract: pluginContract,
|
|
175
|
+
contract_sha256: hash(pluginContract),
|
|
176
|
+
preflight: { passed: true, plugin_sha256: 'plugin-hash' },
|
|
177
|
+
arms: {
|
|
178
|
+
baseline: {
|
|
179
|
+
...arm('fail', true),
|
|
180
|
+
intervention: { skill_text: true, plugin_enforcement: false },
|
|
181
|
+
loaded_skills: ['tdd'],
|
|
182
|
+
},
|
|
183
|
+
treatment: {
|
|
184
|
+
...arm('pass', true),
|
|
185
|
+
intervention: { skill_text: true, plugin_enforcement: true },
|
|
186
|
+
loaded_skills: ['tdd'],
|
|
187
|
+
plugin_sha256: 'plugin-hash',
|
|
188
|
+
plugin_attribution: { fired: true, state_files: ['workspaces/x/state/sessions/y/edit-state.json'] },
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
helper.check(
|
|
193
|
+
'32. plugin pair is includable only when preflight, hash, constant skill text, and per-arm attribution agree',
|
|
194
|
+
validatePairRecord(pluginPair).included,
|
|
195
|
+
JSON.stringify(validatePairRecord(pluginPair)),
|
|
196
|
+
);
|
|
197
|
+
const unattributedPlugin = JSON.parse(JSON.stringify(pluginPair));
|
|
198
|
+
unattributedPlugin.pair_id = 'plugin-unattributed';
|
|
199
|
+
unattributedPlugin.arms.treatment.plugin_attribution.fired = false;
|
|
200
|
+
helper.check(
|
|
201
|
+
'32. missing live plugin attribution is infrastructure/inconclusive, not a behavioral result',
|
|
202
|
+
!validatePairRecord(unattributedPlugin).included && validatePairRecord(unattributedPlugin).reason === 'plugin-attribution-missing',
|
|
203
|
+
JSON.stringify(validatePairRecord(unattributedPlugin)),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const records = [
|
|
207
|
+
skillPair('a-r1', 'fail', 'pass', { caseId: 'a' }),
|
|
208
|
+
skillPair('a-r2', 'fail', 'pass', { caseId: 'a' }),
|
|
209
|
+
skillPair('b-r1', 'pass', 'fail', { caseId: 'b' }),
|
|
210
|
+
skillPair('c-r1', 'pass', 'pass', { caseId: 'c' }),
|
|
211
|
+
skillPair('infra-r1', 'pass', 'session-error', { caseId: 'infra' }),
|
|
212
|
+
];
|
|
213
|
+
const summary = summarizePairs(records, 10);
|
|
214
|
+
helper.check(
|
|
215
|
+
'32. summary excludes infrastructure outcomes and reports paired transitions',
|
|
216
|
+
summary.requested_pairs === 5 && summary.included_pairs === 4 && summary.excluded_pairs === 1 &&
|
|
217
|
+
summary.transitions['fail->pass'] === 2 && summary.transitions['pass->fail'] === 1 &&
|
|
218
|
+
summary.transitions['pass->pass'] === 1 && summary.transitions['fail->fail'] === 0,
|
|
219
|
+
JSON.stringify(summary),
|
|
220
|
+
);
|
|
221
|
+
helper.check(
|
|
222
|
+
'32. paired effect is derived from within-pair transitions and carries a deterministic uncertainty interval',
|
|
223
|
+
summary.paired_effect_pp === 25 && Array.isArray(summary.paired_effect_ci95_pp) && summary.paired_effect_ci95_pp.length === 2,
|
|
224
|
+
JSON.stringify(summary),
|
|
225
|
+
);
|
|
226
|
+
helper.check(
|
|
227
|
+
'32. repeated runs remain separate samples and expose per-case variance instead of overwriting',
|
|
228
|
+
summary.repeat_variance.a.repeats === 2 && summary.repeat_variance.a.paired_difference_variance === 0,
|
|
229
|
+
JSON.stringify(summary.repeat_variance),
|
|
230
|
+
);
|
|
231
|
+
helper.check(
|
|
232
|
+
'32. cost and execution overhead are reported separately from correctness',
|
|
233
|
+
summary.cost_and_execution_overhead.cost_delta.mean === 1 &&
|
|
234
|
+
summary.cost_and_execution_overhead.token_delta.mean === 20 &&
|
|
235
|
+
summary.cost_and_execution_overhead.tool_call_delta.mean === 1 &&
|
|
236
|
+
summary.cost_and_execution_overhead.duration_ms_delta.mean === 200,
|
|
237
|
+
JSON.stringify(summary.cost_and_execution_overhead),
|
|
238
|
+
);
|
|
239
|
+
helper.check(
|
|
240
|
+
'32. exact discordant-pair statistic is available without turning it into an unpaired pass-rate verdict',
|
|
241
|
+
mcnemarExactP(2, 1) === 1 && summary.mcnemar_exact_p === 1,
|
|
242
|
+
JSON.stringify({ direct: mcnemarExactP(2, 1), summary: summary.mcnemar_exact_p }),
|
|
243
|
+
);
|
|
244
|
+
helper.check(
|
|
245
|
+
'32. the predeclared practical threshold is retained in the summary decision contract',
|
|
246
|
+
summary.decision.min_effect_pp === 10 && /predeclared/i.test(summary.decision.rule),
|
|
247
|
+
JSON.stringify(summary.decision),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
helper.finish();
|
|
251
|
+
} catch (error) {
|
|
252
|
+
helper.check('32. paired behavioral benchmark contract', false, error.stack);
|
|
253
|
+
helper.finish();
|
|
254
|
+
}
|
package/docs/architecture.md
CHANGED
|
@@ -100,9 +100,9 @@ The installer configures native lifecycle hooks and project skills.
|
|
|
100
100
|
|
|
101
101
|
The prompt hook is intentionally lightweight: it does **not** prescribe TODO/TDD/Fable order. It gives the model the rails and lets the model orchestrate itself.
|
|
102
102
|
|
|
103
|
-
### 2. OpenCode — plugin enforcement, live
|
|
103
|
+
### 2. OpenCode — plugin enforcement, scoped live verification
|
|
104
104
|
|
|
105
|
-
`opencode-plugin/` maps supported enforcement behavior to OpenCode's real plugin API.
|
|
105
|
+
`opencode-plugin/` maps supported enforcement behavior to OpenCode's real plugin API with deterministic mechanism coverage. Partial live-host evidence supports project-scope `.js` loading and edit/verification state on OpenCode 1.18.31 (macOS): [retained evidence](../benchmarks/results/live-host/opencode-2026-09-16/README.md). The final snapshot is post-reset (`hardLock: false`, `count: 1`), not a full enforcement history. Hard lock is only an interactive observation with no retained blocked-tool trace. Reflection was operator-seeded, then agent-rewritten; state-file deletion is an agent-controlled reset, not durable hard enforcement. This does not establish behavioral effectiveness. `.mjs` auto-discovery is broken on this host version (issue #127). Global scope, npm-package installation, and other host versions remain unverified.
|
|
106
106
|
|
|
107
107
|
### 3. Cursor — skills plus advisory rules
|
|
108
108
|
|
|
@@ -15,7 +15,7 @@ Statuses are intentionally explicit:
|
|
|
15
15
|
- `Unsupported`: official host documentation explicitly rules out the capability.
|
|
16
16
|
- `Unknown`: the capability or its current host behavior was not established.
|
|
17
17
|
|
|
18
|
-
All
|
|
18
|
+
All `liveHostVerification` rows except OpenCode are `Unknown`. OpenCode's row is `Partial`: retained evidence supports project-scope `.js` loading and edit/verification state on OpenCode 1.18.31 (macOS), not durable hard enforcement. No fresh artifacts exist yet for the other surfaces, managed-workspace import, or public OpenAI approval.
|
|
19
19
|
|
|
20
20
|
## Current capability summary
|
|
21
21
|
|
|
@@ -24,7 +24,7 @@ All current `liveHostVerification` rows are `Unknown`. This checkout contains de
|
|
|
24
24
|
| Codex | `Supported` | `Mechanism verified` | `Mechanism verified` for the local OpenAI package, including session, prompt, supported-tool, subagent, and stop hooks | `.agents/skills/`, `AGENTS.md` | `~/.agents/skills/` | Local package hooks are mechanically checked; live loading and marketplace refresh remain `Unknown`. |
|
|
25
25
|
| ChatGPT / OpenAI Plugin | `Unknown` for a repository-local standalone path | `Mechanism verified` | `Partial`: local Work/Codex hooks are separate from ordinary Chat | Managed workspace marketplace | Not established | The public Skills-only bundle is mechanically reproducible; import, approval, and live behavior remain `Unknown`. |
|
|
26
26
|
| Claude Code | `Supported` | `Supported` | `Mechanism verified` for Harness configuration | `.claude/skills/` | `~/.claude/skills/` | Native Claude surfaces are documented and package/installer checks pass; fresh-host execution is not preserved here. |
|
|
27
|
-
| OpenCode | `Supported` | `Mechanism verified` | `
|
|
27
|
+
| OpenCode | `Supported` | `Partial` live-host loading for the project-scope, `.js`-filename install path on OpenCode 1.18.31 (macOS); other install paths remain `Mechanism verified` | `Partial` live-host evidence for project-scope `.js` loading and edit/verification state; hard lock reported only, with no retained blocked-tool trace and a post-reset final snapshot | `.opencode/skills/`, `.agents/skills/`, `.opencode/plugins/` | `~/.config/opencode/skills/`, `~/.agents/skills/`, `~/.config/opencode/plugins/` | [Retained evidence](../benchmarks/results/live-host/opencode-2026-09-16/README.md): loading and state effects verified in one host session; final snapshot post-reset; hard lock only an interactive observation; reflection operator-seeded, then agent-rewritten; no raw transcript or behavioral-effectiveness claim. `.mjs` auto-discovery broken (issue #127); global scope, npm-package installation, and other host versions remain unverified. |
|
|
28
28
|
| GitHub Copilot agent surfaces | `Supported` | `Unknown` for a Harness-specific plugin install | `Unknown` | `.github/skills/`, `.agents/skills/`, repository instructions | `~/.copilot/skills/`, `~/.agents/skills/` | GitHub Agent Skills paths and installer behavior are checked; no live Copilot session or plugin install is preserved. |
|
|
29
29
|
| Cursor | `Supported` | `Mechanism verified` for the portable package shape | `Unknown` for Harness | `.cursor/skills/`, `.agents/skills/` | `~/.cursor/skills/`, `~/.agents/skills/` | Official paths and package shape are checked; Cursor plugin loading and hooks remain unverified. |
|
|
30
30
|
| Continue.dev | `Unknown` for standalone `SKILL.md` discovery | `Unknown` | `Unknown` | `.continue/rules/` is documented; `.continue/skills/` is only an installer candidate | `~/.continue/skills/` is only an installer candidate | The reviewed official docs establish rules, not an Agent Skills host contract. |
|
|
@@ -64,7 +64,7 @@ The general installer merges shared files only through Harness-owned markers and
|
|
|
64
64
|
|
|
65
65
|
## OpenCode boundary
|
|
66
66
|
|
|
67
|
-
The OpenCode adapter uses the documented plugin API and has deterministic mechanism coverage.
|
|
67
|
+
The OpenCode adapter uses the documented plugin API and has deterministic mechanism coverage. Retained evidence supports project-scope `.js` loading and edit/verification state on OpenCode 1.18.31 (macOS): [live-host evidence](../benchmarks/results/live-host/opencode-2026-09-16/README.md). The final snapshot is post-reset (`hardLock: false`, `count: 1`), not a history of the enforcement sequence. Hard lock is only an interactive observation with no retained blocked-tool trace. Reflection was operator-seeded, then agent-rewritten. Agent-controlled state deletion resets the breaker, so do not promote this into a durable hard-enforcement or behavioral-effectiveness claim. `.mjs` auto-discovery is broken on this host version (issue #127). Global scope, npm-package installation, and other host versions remain unverified.
|
|
68
68
|
|
|
69
69
|
## Official sources
|
|
70
70
|
|
|
@@ -172,15 +172,15 @@
|
|
|
172
172
|
],
|
|
173
173
|
"capabilities": {
|
|
174
174
|
"standaloneSkills": {"status": "Supported", "paths": {"project": [".opencode/skills", ".agents/skills", ".claude/skills"], "global": ["~/.config/opencode/skills", "~/.agents/skills", "~/.claude/skills"]}},
|
|
175
|
-
"pluginSupport": {"status": "
|
|
175
|
+
"pluginSupport": {"status": "Partial", "paths": {"project": [".opencode/plugins"], "global": ["~/.config/opencode/plugins"]}, "notes": "Live verified scoped: project-scope install of the plugin copied under a .js filename loads on OpenCode 1.18.31 (macOS) and edit/verification state effects are retained (benchmarks/results/live-host/opencode-2026-09-16/README.md). The host discovery silently ignores .mjs install names (issue #127). Global scope and npm-package installation remain mechanism-only."},
|
|
176
176
|
"bundledSkills": {"status": "Partial", "notes": "Harness provides a standalone enforcement module; it is not an OpenCode bundled-skill package."},
|
|
177
|
-
"hooksLifecycle": {"status": "
|
|
178
|
-
"projectScope": {"status": "
|
|
177
|
+
"hooksLifecycle": {"status": "Partial", "notes": "Live-host evidence covers project-scope .js loading and edit/verification state (edit tracking, forced verification, breaker failure recording) on OpenCode 1.18.31 (macOS); hard lock is only an interactive observation with no retained blocked-tool trace and the final snapshot is post-reset (benchmarks/results/live-host/opencode-2026-09-16/README.md). Global scope, npm-package installation, and other host versions remain mechanism-tested."},
|
|
178
|
+
"projectScope": {"status": "Partial", "paths": {"skills": [".opencode/skills", ".agents/skills"], "plugins": [".opencode/plugins"]}, "notes": "Plugin loading at project scope and edit/verification state are live-verified via the retained host session artifact; standalone skill discovery remains path-contract verified."},
|
|
179
179
|
"globalScope": {"status": "Mechanism verified", "paths": {"skills": ["~/.config/opencode/skills", "~/.agents/skills"], "plugins": ["~/.config/opencode/plugins"]}},
|
|
180
180
|
"installUninstallSymmetry": {"status": "Unknown", "notes": "The adapter is manually copied; Harness does not own an OpenCode install/uninstall flow."},
|
|
181
181
|
"assetsReferences": {"status": "Mechanism verified", "notes": "The adapter is self-contained and skill references are separately checked."},
|
|
182
182
|
"updateSync": {"status": "Unknown", "notes": "No live OpenCode refresh or npm plugin update is preserved."},
|
|
183
|
-
"liveHostVerification": {"status": "
|
|
183
|
+
"liveHostVerification": {"status": "Partial", "evidence": "benchmarks/results/live-host/opencode-2026-09-16/README.md", "notes": "Retained evidence supports project-scope .js plugin loading and edit/verification state on OpenCode 1.18.31 (macOS), not durable hard enforcement: final snapshot is post-reset (hardLock: false), hard lock only an interactive observation with no retained blocked-tool trace, reflection operator-seeded, agent-controlled state deletion resets the breaker. Global scope, npm-package installation, and other host versions remain unverified."}
|
|
184
184
|
}
|
|
185
185
|
},
|
|
186
186
|
{
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Adds hard enforcement gates for Harness skills in opencode, addressing the
|
|
4
4
|
limitation that skills are otherwise advisory-only on that platform.
|
|
5
5
|
|
|
6
|
-
> **Evidence boundary:** the plugin is implemented against opencode's real plugin API and covered by deterministic mechanism tests.
|
|
6
|
+
> **Evidence boundary:** the plugin is implemented against opencode's real plugin API and covered by deterministic mechanism tests. Live plugin loading remains unverified beyond one scoped surface: retained evidence supports project-scope `.js` loading and edit/verification state on OpenCode 1.18.31 (macOS) — [live-host evidence](../benchmarks/results/live-host/opencode-2026-09-16/README.md). The final snapshot is post-reset (`hardLock: false`, `count: 1`), not a history of the enforcement sequence. Hard lock is only an interactive observation with no retained blocked-tool trace. Reflection was operator-seeded, then agent-rewritten; agent-controlled state deletion resets the breaker, so no durable hard enforcement or behavioral effectiveness is claimed. Global scope, npm-package installation, and other host versions remain unverified. See [`../docs/platform-capabilities.md`](../docs/platform-capabilities.md).
|
|
7
7
|
|
|
8
8
|
## Problem
|
|
9
9
|
|
|
@@ -20,15 +20,18 @@ actually loads: a JS/TS file exporting a function that returns a hooks
|
|
|
20
20
|
object (see https://opencode.ai/docs/plugins/). There is no manifest-to-script
|
|
21
21
|
mechanism; an earlier version of this plugin assumed one (a `plugin.json`
|
|
22
22
|
mapping event names to standalone scripts) and opencode never invoked it -
|
|
23
|
-
see issue #37 for how that was found and fixed.
|
|
23
|
+
see issue #37 for how that was found and fixed. **Naming caveat (issue
|
|
24
|
+
#127):** opencode's discovery glob only matches `*.js`/`*.ts`, so the file
|
|
25
|
+
must be copied to a `.js` name when installed - see Installation below.
|
|
24
26
|
|
|
25
27
|
It implements three enforcement mechanisms across opencode's real hooks:
|
|
26
28
|
|
|
27
29
|
The module follows opencode's V1 plugin contract directly: the named
|
|
28
30
|
`HarnessEnforcement` export is an async factory receiving the opencode context
|
|
29
31
|
(`client`, `directory`, `project`, `worktree`, and `$`) and returning the hook
|
|
30
|
-
map. Copying this one
|
|
31
|
-
opencode to discover and invoke it; `plugin.json` is only a
|
|
32
|
+
map. Copying this one file into `.opencode/plugins/` under a `.js` name is
|
|
33
|
+
enough for opencode to discover and invoke it; `plugin.json` is only a
|
|
34
|
+
repository
|
|
32
35
|
inventory and is not an opencode runtime manifest. The hook callbacks use the
|
|
33
36
|
V1 `(input, output)` shape, including
|
|
34
37
|
`output.args` when the host supplies tool arguments only in the result object.
|
|
@@ -73,18 +76,30 @@ new breaker failure. A reflection artifact itself does not count as a code edit.
|
|
|
73
76
|
|
|
74
77
|
## Installation
|
|
75
78
|
|
|
76
|
-
Copy `index.mjs` into opencode's plugin directory
|
|
77
|
-
sibling files required:
|
|
79
|
+
Copy `index.mjs` into opencode's plugin directory **under a `.js` filename** -
|
|
80
|
+
it is self-contained, no sibling files required:
|
|
78
81
|
|
|
79
82
|
```bash
|
|
80
|
-
|
|
83
|
+
mkdir -p .opencode/plugins
|
|
84
|
+
cp opencode-plugin/index.mjs .opencode/plugins/harness-enforcement.js # project-level
|
|
81
85
|
# or
|
|
82
|
-
|
|
86
|
+
mkdir -p ~/.config/opencode/plugins
|
|
87
|
+
cp opencode-plugin/index.mjs ~/.config/opencode/plugins/harness-enforcement.js # global
|
|
83
88
|
```
|
|
84
89
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
Restart OpenCode after copying so the plugin is discovered at startup.
|
|
91
|
+
|
|
92
|
+
The destination extension matters: opencode's plugin auto-discovery scans
|
|
93
|
+
only `*.js` / `*.ts` files in those directories (`{plugin,plugins}/*.{ts,js}`
|
|
94
|
+
in its v1 loader), so a `.mjs` copy is **silently never loaded** - no error,
|
|
95
|
+
no hooks, no state. Verified live on opencode 1.18.31 with an extension A/B
|
|
96
|
+
probe (see
|
|
97
|
+
[`benchmarks/results/live-host/opencode-2026-09-16/probe-control.md`](../benchmarks/results/live-host/opencode-2026-09-16/probe-control.md)).
|
|
98
|
+
|
|
99
|
+
opencode auto-loads `.js`/`.ts` files dropped in those directories at startup -
|
|
100
|
+
no `opencode.json` entry needed. This procedure tests directory discovery;
|
|
101
|
+
explicit local-file configuration and npm-package installation are separate
|
|
102
|
+
installation surfaces and were not verified in this run.
|
|
88
103
|
|
|
89
104
|
## Manual verification CLI
|
|
90
105
|
|
|
@@ -135,3 +150,9 @@ fail-closed behavior, and patch-based reflection writes.
|
|
|
135
150
|
It does not launch a real opencode process; the test remains a deterministic
|
|
136
151
|
hook-sequence check against the documented and source-verified hook signatures.
|
|
137
152
|
A passing test is therefore mechanism evidence, not live-host loading evidence.
|
|
153
|
+
Live-host evidence and the install-filename loadability guard live elsewhere:
|
|
154
|
+
the host session artifact is
|
|
155
|
+
[`benchmarks/results/live-host/opencode-2026-09-16/`](../benchmarks/results/live-host/opencode-2026-09-16/),
|
|
156
|
+
and `ci/mechanism-30-opencode-plugin-loadability.test.js` (npm script
|
|
157
|
+
`test:opencode:loadability`) fails any change that reintroduces a
|
|
158
|
+
silently-ignored install filename such as `harness-enforcement.mjs`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-everything",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Harness OS: An Orchestrated Agent Operating System for AI-driven Development",
|
|
5
5
|
"main": "harness-everything/scripts/bootstrap.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"test": "node self-evolve/scripts/self-regression.js",
|
|
11
11
|
"self-regression": "node self-evolve/scripts/self-regression.js",
|
|
12
12
|
"test:mechanism": "node ci/mechanism-test.js",
|
|
13
|
+
"test:opencode:loadability": "node ci/mechanism-30-opencode-plugin-loadability.test.js",
|
|
13
14
|
"test:mutations": "node ci/mutation-check.js",
|
|
14
15
|
"test:tdd-quality": "node ci/mechanism-2s-tdd-quality.test.js",
|
|
15
16
|
"tdd:quality": "node tdd/scripts/quality-gate.js",
|
|
@@ -39,6 +40,9 @@
|
|
|
39
40
|
"eval:behavioral": "node behavioral-evals/run.js run",
|
|
40
41
|
"eval:behavioral:validate": "node behavioral-evals/run.js validate",
|
|
41
42
|
"eval:behavioral:plugin": "node behavioral-evals/run-with-plugin.js run",
|
|
43
|
+
"eval:opencode:hardlock-live": "node behavioral-evals/opencode-hardlock-live.js run",
|
|
44
|
+
"eval:paired": "node behavioral-evals/paired-benchmark.js run",
|
|
45
|
+
"eval:paired:summarize": "node behavioral-evals/paired-benchmark.js summarize",
|
|
42
46
|
"test:ab": "node ci/ab-test-harness.js validate",
|
|
43
47
|
"test:ab:run": "node ci/ab-test-harness.js run",
|
|
44
48
|
"harness:reset": "node hooks/scripts/reset-circuit-breaker.js",
|
|
@@ -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.6.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",
|