harness-everything 0.41.2 → 0.42.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 +12 -0
- package/benchmarks/results/h1-provider-independence-qualification/README.md +72 -0
- package/ci/action-gate.test.js +24 -0
- package/ci/mechanism-325-3b-h1-qualification.test.js +206 -0
- package/ci/mechanism-341-opencode-lane.test.js +465 -0
- package/ci/mechanism-codex-prompt-pass-through.test.js +94 -0
- package/ci/mechanism-task-git-authorization.test.js +145 -0
- package/docs/codex-action-gate.md +28 -21
- package/docs/platform-capabilities.md +20 -16
- package/hooks/scripts/action-gate.js +18 -47
- package/hooks/scripts/codex-user-prompt.js +4 -16
- package/hooks/scripts/lib/doc-reconciliation-pilot.js +40 -10
- package/hooks/scripts/lib/doc-reconciliation-provider.js +159 -11
- package/hooks/scripts/workflow-stop-gate.js +4 -1
- package/opencode-plugin/index.mjs +77 -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/hooks/scripts/action-gate.js +18 -47
- package/plugins/harness-everything/hooks/scripts/codex-user-prompt.js +4 -16
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js +40 -10
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +159 -11
- package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +4 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/sbom.cdx.json +7 -7
- package/ci/mechanism-codex-action-authorization.test.js +0 -292
- package/hooks/scripts/lib/codex-action-authorization.js +0 -193
- package/plugins/harness-everything/hooks/scripts/lib/codex-action-authorization.js +0 -193
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const { spawnSync } = require('node:child_process');
|
|
9
|
+
const { pathToFileURL } = require('node:url');
|
|
10
|
+
const { test } = require('node:test');
|
|
11
|
+
|
|
12
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
13
|
+
const STOP_GATE = path.join(ROOT, 'hooks/scripts/workflow-stop-gate.js');
|
|
14
|
+
const PILOT = path.join(ROOT, 'hooks/scripts/lib/doc-reconciliation-pilot.js');
|
|
15
|
+
const savedEnv = Object.fromEntries(['HOME', 'HARNESS_STATE_HOME', 'HARNESS_WORKSPACE_ROOT', 'HARNESS_DOC_RECONCILIATION_PILOT'].map(key => [key, process.env[key]]));
|
|
16
|
+
|
|
17
|
+
function git(cwd, args) {
|
|
18
|
+
const env = { ...process.env, GIT_AUTHOR_NAME: 'Harness test', GIT_AUTHOR_EMAIL: 'harness@example.invalid', GIT_COMMITTER_NAME: 'Harness test', GIT_COMMITTER_EMAIL: 'harness@example.invalid' };
|
|
19
|
+
for (const key of Object.keys(env)) if (key.startsWith('GIT_') && !['GIT_AUTHOR_NAME', 'GIT_AUTHOR_EMAIL', 'GIT_COMMITTER_NAME', 'GIT_COMMITTER_EMAIL'].includes(key)) delete env[key];
|
|
20
|
+
return spawnSync('git', ['-C', cwd, ...args], { encoding: 'utf8', env });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function runJson(script, cwd, input, env = {}) {
|
|
24
|
+
return spawnSync(process.execPath, [script], {
|
|
25
|
+
cwd: ROOT,
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
input: JSON.stringify(input),
|
|
28
|
+
env: { ...process.env, ...env },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeFixture() {
|
|
33
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-341-opencode-workspace-'));
|
|
34
|
+
const stateHome = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-341-opencode-state-'));
|
|
35
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-341-opencode-home-'));
|
|
36
|
+
fs.mkdirSync(path.join(root, '.agents/harness-everything'), { recursive: true });
|
|
37
|
+
fs.mkdirSync(path.join(root, 'docs/adr'), { recursive: true });
|
|
38
|
+
fs.writeFileSync(path.join(root, '.agents/harness-everything/project-docs.json'), JSON.stringify({ governed: [{ path: 'docs/adr/**', role: 'adr' }], domains: ['GRPH'] }, null, 2) + '\n');
|
|
39
|
+
fs.writeFileSync(path.join(root, 'docs/adr/GRPH-001.md'), '# GRPH-001 Retry\n\nCheckout MUST retry once.\n');
|
|
40
|
+
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'fixture', version: '1.0.0', scripts: { test: 'node -e "process.exit(0)"' } }, null, 2));
|
|
41
|
+
const init = git(root, ['init', '-q']);
|
|
42
|
+
assert.equal(init.status, 0, init.stderr);
|
|
43
|
+
assert.equal(git(root, ['add', '-A']).status, 0);
|
|
44
|
+
assert.equal(git(root, ['-c', 'commit.gpgsign=false', 'commit', '-q', '-m', 'baseline']).status, 0);
|
|
45
|
+
fs.writeFileSync(path.join(root, 'docs/adr/GRPH-001.md'), '# GRPH-001 Retry\n\nCheckout MUST retry twice.\n');
|
|
46
|
+
process.env.HOME = home;
|
|
47
|
+
process.env.HARNESS_STATE_HOME = stateHome;
|
|
48
|
+
process.env.HARNESS_WORKSPACE_ROOT = root;
|
|
49
|
+
process.env.HARNESS_DOC_RECONCILIATION_PILOT = '1';
|
|
50
|
+
return {
|
|
51
|
+
root, stateHome, home,
|
|
52
|
+
cleanup: () => {
|
|
53
|
+
for (const directory of [root, stateHome, home]) fs.rmSync(directory, { recursive: true, force: true });
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function seedWorkflow(fixture, sessionId) {
|
|
59
|
+
const payload = { session_id: sessionId, cwd: fixture.root, host_id: 'opencode' };
|
|
60
|
+
const { getSessionDir } = require(path.join(ROOT, 'hooks/scripts/lib/harness-state.js'));
|
|
61
|
+
const sessionDir = getSessionDir(fixture.root, sessionId, payload);
|
|
62
|
+
const plan = { strategySelection: 'selected', tier: 'tier2', strategy: 'iterative-single' };
|
|
63
|
+
const context = {
|
|
64
|
+
root: fixture.root,
|
|
65
|
+
sessionId,
|
|
66
|
+
sessionDir,
|
|
67
|
+
file: path.join(sessionDir, 'workflow-run.json'),
|
|
68
|
+
workflow: {
|
|
69
|
+
schemaVersion: 1,
|
|
70
|
+
workflowId: `wf-${sessionId}`,
|
|
71
|
+
revision: 1,
|
|
72
|
+
sessionId,
|
|
73
|
+
tier: 'tier2',
|
|
74
|
+
strategy: 'iterative-single',
|
|
75
|
+
state: 'running',
|
|
76
|
+
workflowPlan: plan,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
const obligations = require(path.join(ROOT, 'hooks/scripts/lib/workflow-obligations.js'));
|
|
80
|
+
obligations.initializePlanningContract(context, plan);
|
|
81
|
+
obligations.recordPlanning(context, JSON.stringify([
|
|
82
|
+
{ id: 'edit-doc', stepType: 'behavior-change', summary: 'Revise the governed retry requirement', acceptance: 'the normative requirement is updated' },
|
|
83
|
+
{ id: 'verify-doc', stepType: 'verification', summary: 'Verify the governed retry change', acceptance: 'the objective checks are recorded' },
|
|
84
|
+
]), plan, plan.strategy, 'fixture workflow for the OpenCode reconciliation path');
|
|
85
|
+
obligations.materializeExecutionObligations(context, plan);
|
|
86
|
+
require(PILOT).captureBaseline(context);
|
|
87
|
+
require(path.join(ROOT, 'hooks/scripts/lib/workflow-runtime.js')).saveWorkflow(context);
|
|
88
|
+
return { sessionDir, payload };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function writeEditState(sessionDir, values) {
|
|
92
|
+
fs.writeFileSync(path.join(sessionDir, 'edit-state.json'), JSON.stringify({
|
|
93
|
+
lastEditTime: null,
|
|
94
|
+
verificationPending: false,
|
|
95
|
+
editsSinceVerification: 0,
|
|
96
|
+
followUpPending: false,
|
|
97
|
+
followUpDeliveryPending: false,
|
|
98
|
+
followUpMessage: null,
|
|
99
|
+
lastFollowUpError: null,
|
|
100
|
+
sessionStart: Date.now(),
|
|
101
|
+
...values,
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function createEventStream() {
|
|
106
|
+
const queued = [];
|
|
107
|
+
const waiters = [];
|
|
108
|
+
let last = null;
|
|
109
|
+
let closed = false;
|
|
110
|
+
return {
|
|
111
|
+
subscribe(signal) {
|
|
112
|
+
const iterator = {
|
|
113
|
+
next() {
|
|
114
|
+
if (last) { last.resolve(); last = null; }
|
|
115
|
+
if (queued.length) {
|
|
116
|
+
last = queued.shift();
|
|
117
|
+
return Promise.resolve({ value: last.value, done: false });
|
|
118
|
+
}
|
|
119
|
+
if (closed) return Promise.resolve({ done: true });
|
|
120
|
+
return new Promise(resolve => waiters.push(resolve));
|
|
121
|
+
},
|
|
122
|
+
return() { closed = true; return Promise.resolve({ done: true }); },
|
|
123
|
+
};
|
|
124
|
+
signal.addEventListener('abort', () => {
|
|
125
|
+
closed = true;
|
|
126
|
+
while (waiters.length) waiters.shift()({ done: true });
|
|
127
|
+
}, { once: true });
|
|
128
|
+
return { [Symbol.asyncIterator]: () => iterator };
|
|
129
|
+
},
|
|
130
|
+
async dispatch(value) {
|
|
131
|
+
await new Promise(resolve => {
|
|
132
|
+
const item = { value, resolve };
|
|
133
|
+
if (waiters.length) {
|
|
134
|
+
if (last) { last.resolve(); last = null; }
|
|
135
|
+
last = item;
|
|
136
|
+
waiters.shift()({ value, done: false });
|
|
137
|
+
} else queued.push(item);
|
|
138
|
+
});
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function startPlugin(workspace) {
|
|
144
|
+
const plugin = await import(pathToFileURL(path.join(ROOT, 'opencode-plugin/index.mjs')).href);
|
|
145
|
+
const registered = new Map();
|
|
146
|
+
const stream = createEventStream();
|
|
147
|
+
const dispose = await plugin.default.setup({
|
|
148
|
+
location: { directory: workspace },
|
|
149
|
+
tool: { hook: async (name, callback) => registered.set(name, callback) },
|
|
150
|
+
session: { prompt: async () => ({}) },
|
|
151
|
+
event: { subscribe: ({ signal }) => stream.subscribe(signal) },
|
|
152
|
+
});
|
|
153
|
+
return { registered, stream, dispose };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function canonicalJson(value) {
|
|
157
|
+
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
158
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonicalJson(value[key])]));
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function restoreEnv() {
|
|
163
|
+
for (const [key, value] of Object.entries(savedEnv)) {
|
|
164
|
+
if (value === undefined) delete process.env[key];
|
|
165
|
+
else process.env[key] = value;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
test('existing Stop consumer writes a refused report without qualification', () => {
|
|
170
|
+
const fixture = makeFixture();
|
|
171
|
+
try {
|
|
172
|
+
const control = seedWorkflow(fixture, 'stop-positive-control');
|
|
173
|
+
const result = runJson(STOP_GATE, fixture.root, control.payload);
|
|
174
|
+
const reportPath = path.join(control.sessionDir, 'doc-reconciliation-pilot.json');
|
|
175
|
+
const report = fs.existsSync(reportPath) ? JSON.parse(fs.readFileSync(reportPath, 'utf8')) : null;
|
|
176
|
+
assert.equal(result.status, 0, result.stderr);
|
|
177
|
+
assert.equal(report?.pilotStatus, 'unresolved');
|
|
178
|
+
assert.equal(report?.pilotCredit, false);
|
|
179
|
+
assert.ok(report?.diagnostics?.length, 'existing consumer must retain a refusal diagnostic');
|
|
180
|
+
} finally {
|
|
181
|
+
fixture.cleanup();
|
|
182
|
+
restoreEnv();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('OpenCode capture → idle Stop whole path is refused without live qualification', async () => {
|
|
187
|
+
const fixture = makeFixture();
|
|
188
|
+
const findings = [];
|
|
189
|
+
let dispose;
|
|
190
|
+
let originalAssess;
|
|
191
|
+
try {
|
|
192
|
+
const sessionId = 'native-v2-whole-path';
|
|
193
|
+
const { sessionDir } = seedWorkflow(fixture, sessionId);
|
|
194
|
+
const pilot = require(PILOT);
|
|
195
|
+
originalAssess = pilot.assess;
|
|
196
|
+
const assessmentSessions = [];
|
|
197
|
+
pilot.assess = function (context, ...args) {
|
|
198
|
+
assessmentSessions.push(context?.sessionId);
|
|
199
|
+
return originalAssess.call(this, context, ...args);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const plugin = await startPlugin(fixture.root);
|
|
203
|
+
dispose = plugin.dispose;
|
|
204
|
+
const nativeResult = { output: 'private-result-body', metadata: { exit: 0 } };
|
|
205
|
+
await plugin.registered.get('execute.after')({
|
|
206
|
+
tool: 'edit', sessionID: sessionId, id: 'call-1', messageID: 'message-1',
|
|
207
|
+
status: 'completed', result: nativeResult,
|
|
208
|
+
input: { filePath: path.join(fixture.root, 'docs/adr/GRPH-001.md') },
|
|
209
|
+
});
|
|
210
|
+
await plugin.registered.get('execute.after')({
|
|
211
|
+
tool: 'bash', sessionID: sessionId, id: 'call-2', messageID: 'message-2',
|
|
212
|
+
status: 'completed', error: { name: 'NativeError', message: 'native command error', code: 'NATIVE_FAILURE' },
|
|
213
|
+
input: { command: 'doc-check' },
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const journal = path.join(sessionDir, 'opencode-captures.jsonl');
|
|
217
|
+
let records = [];
|
|
218
|
+
if (fs.existsSync(journal)) {
|
|
219
|
+
const lines = fs.readFileSync(journal, 'utf8').trim().split(/\r?\n/);
|
|
220
|
+
records = lines.map(line => JSON.parse(line));
|
|
221
|
+
}
|
|
222
|
+
const canonical = JSON.stringify(canonicalJson(nativeResult));
|
|
223
|
+
const record = records[0];
|
|
224
|
+
const errorRecord = records[1];
|
|
225
|
+
if (records.length !== 2 || !record || record.hostId !== 'opencode' || record.session_id !== sessionId || record.call_id !== 'call-1' ||
|
|
226
|
+
record.message_id !== 'message-1' || record.status !== 'completed' || record.exit !== 0 ||
|
|
227
|
+
record.cwd !== undefined || record.result_digest !== crypto.createHash('sha256').update(canonical).digest('hex') ||
|
|
228
|
+
record.result_bytes !== Buffer.byteLength(canonical) || fs.readFileSync(journal, 'utf8').includes('private-result-body')) {
|
|
229
|
+
findings.push(`native result was not retained as one host-labelled digest-only record: ${JSON.stringify(record)}`);
|
|
230
|
+
}
|
|
231
|
+
if (!errorRecord || errorRecord.hostId !== 'opencode' || errorRecord.call_id !== 'call-2' ||
|
|
232
|
+
errorRecord.error?.name !== 'NativeError' || errorRecord.error?.message !== 'native command error' ||
|
|
233
|
+
errorRecord.error?.code !== 'NATIVE_FAILURE' || errorRecord.exit === 0) {
|
|
234
|
+
findings.push(`native error was not preserved as failure without fabricated exit zero: ${JSON.stringify(errorRecord)}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await plugin.stream.dispatch({ type: 'session.idle', sessionID: sessionId });
|
|
238
|
+
const reportPath = path.join(sessionDir, 'doc-reconciliation-pilot.json');
|
|
239
|
+
const report = fs.existsSync(reportPath) ? JSON.parse(fs.readFileSync(reportPath, 'utf8')) : null;
|
|
240
|
+
if (assessmentSessions.filter(id => id === sessionId).length !== 1) {
|
|
241
|
+
findings.push(`idle Stop dispatch count=${assessmentSessions.filter(id => id === sessionId).length}, expected exactly one`);
|
|
242
|
+
}
|
|
243
|
+
if (!report || report.pilotCredit !== false || report.pilotStatus !== 'unresolved' || !report.diagnostics?.length) {
|
|
244
|
+
findings.push(`idle did not write the actual refused/unavailable report: ${JSON.stringify(report)}`);
|
|
245
|
+
}
|
|
246
|
+
assert.deepEqual(findings, []);
|
|
247
|
+
} finally {
|
|
248
|
+
if (dispose) dispose();
|
|
249
|
+
if (originalAssess) require(PILOT).assess = originalAssess;
|
|
250
|
+
fixture.cleanup();
|
|
251
|
+
restoreEnv();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('session idle dispatches the real consumer once across every existing early return', async () => {
|
|
256
|
+
const fixture = makeFixture();
|
|
257
|
+
let dispose;
|
|
258
|
+
const findings = [];
|
|
259
|
+
const pilot = require(PILOT);
|
|
260
|
+
const originalAssess = pilot.assess;
|
|
261
|
+
const assessmentSessions = [];
|
|
262
|
+
pilot.assess = function (context, ...args) {
|
|
263
|
+
assessmentSessions.push(context?.sessionId);
|
|
264
|
+
return originalAssess.call(this, context, ...args);
|
|
265
|
+
};
|
|
266
|
+
try {
|
|
267
|
+
const plugin = await startPlugin(fixture.root);
|
|
268
|
+
dispose = plugin.dispose;
|
|
269
|
+
const cases = [
|
|
270
|
+
['follow-up-pending', { followUpPending: true }],
|
|
271
|
+
['follow-up-delivery-pending', { followUpDeliveryPending: true, followUpMessage: 'deliver once' }],
|
|
272
|
+
['no-verification-pending', { verificationPending: false, editsSinceVerification: 0 }],
|
|
273
|
+
['verification-succeeds', { verificationPending: true, editsSinceVerification: 1 }],
|
|
274
|
+
];
|
|
275
|
+
for (const [suffix, state] of cases) {
|
|
276
|
+
const sessionId = `idle-${suffix}`;
|
|
277
|
+
const { sessionDir } = seedWorkflow(fixture, sessionId);
|
|
278
|
+
writeEditState(sessionDir, state);
|
|
279
|
+
await plugin.stream.dispatch({ type: 'session.idle', sessionID: sessionId });
|
|
280
|
+
const reportPath = path.join(sessionDir, 'doc-reconciliation-pilot.json');
|
|
281
|
+
const report = fs.existsSync(reportPath) ? JSON.parse(fs.readFileSync(reportPath, 'utf8')) : null;
|
|
282
|
+
if (assessmentSessions.filter(id => id === sessionId).length !== 1) {
|
|
283
|
+
findings.push(`${suffix}: consumer assessment count=${assessmentSessions.filter(id => id === sessionId).length}, expected exactly one`);
|
|
284
|
+
}
|
|
285
|
+
if (!report || report.pilotCredit !== false || report.pilotStatus !== 'unresolved') {
|
|
286
|
+
findings.push(`${suffix}: expected one refused/unavailable report, received ${JSON.stringify(report)}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const cleanupSession = 'idle-no-verification-pending';
|
|
290
|
+
await plugin.stream.dispatch({ type: 'session.idle', sessionID: cleanupSession });
|
|
291
|
+
if (assessmentSessions.filter(id => id === cleanupSession).length !== 2) {
|
|
292
|
+
findings.push(`finally did not release the per-session idle guard for a second event: ${assessmentSessions.filter(id => id === cleanupSession).length} assessments`);
|
|
293
|
+
}
|
|
294
|
+
assert.deepEqual(findings, []);
|
|
295
|
+
} finally {
|
|
296
|
+
if (dispose) dispose();
|
|
297
|
+
pilot.assess = originalAssess;
|
|
298
|
+
fixture.cleanup();
|
|
299
|
+
restoreEnv();
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('V1/no-status, pilot-OFF, no-workflow, and Claude host controls stay fail-closed', async () => {
|
|
304
|
+
const fixture = makeFixture();
|
|
305
|
+
let dispose;
|
|
306
|
+
try {
|
|
307
|
+
const plugin = await startPlugin(fixture.root);
|
|
308
|
+
dispose = plugin.dispose;
|
|
309
|
+
|
|
310
|
+
const v1Session = 'v1-no-status';
|
|
311
|
+
const { sessionDir: v1Dir } = seedWorkflow(fixture, v1Session);
|
|
312
|
+
await plugin.registered.get('execute.after')({
|
|
313
|
+
tool: 'edit', sessionID: v1Session, id: 'v1-call', input: { filePath: path.join(fixture.root, 'docs/adr/GRPH-001.md') },
|
|
314
|
+
});
|
|
315
|
+
assert.equal(fs.existsSync(path.join(v1Dir, 'opencode-captures.jsonl')), false, 'V1/no-status output must not create a native capture');
|
|
316
|
+
|
|
317
|
+
const noWorkflow = 'no-workflow';
|
|
318
|
+
const { getSessionDir } = require(path.join(ROOT, 'hooks/scripts/lib/harness-state.js'));
|
|
319
|
+
const noWorkflowDir = getSessionDir(fixture.root, noWorkflow, { session_id: noWorkflow, cwd: fixture.root, host_id: 'opencode' });
|
|
320
|
+
await plugin.stream.dispatch({ type: 'session.idle', sessionID: noWorkflow });
|
|
321
|
+
assert.equal(fs.existsSync(path.join(noWorkflowDir, 'doc-reconciliation-pilot.json')), false, 'no workflow must not write a pilot report');
|
|
322
|
+
|
|
323
|
+
const offSession = 'pilot-off';
|
|
324
|
+
const { sessionDir: offDir } = seedWorkflow(fixture, offSession);
|
|
325
|
+
process.env.HARNESS_DOC_RECONCILIATION_PILOT = '0';
|
|
326
|
+
await plugin.stream.dispatch({ type: 'session.idle', sessionID: offSession });
|
|
327
|
+
assert.equal(fs.existsSync(path.join(offDir, 'doc-reconciliation-pilot.json')), false, 'pilot OFF must not write a reconciliation report');
|
|
328
|
+
|
|
329
|
+
const { createClaudeOriginProvider } = require(path.join(ROOT, 'hooks/scripts/lib/doc-reconciliation-provider.js'));
|
|
330
|
+
const claudeProvider = createClaudeOriginProvider({ hostId: 'opencode', sessionId: 'native-host-control' });
|
|
331
|
+
const claudeContext = claudeProvider.resolveContext({ workspaceRoot: fixture.root });
|
|
332
|
+
assert.equal(claudeContext.status, 'unavailable', 'OpenCode must not enter through Claude provider semantics');
|
|
333
|
+
assert.ok(claudeContext.diagnostics.some(item => item.code === 'unsupported-host'));
|
|
334
|
+
} finally {
|
|
335
|
+
if (dispose) dispose();
|
|
336
|
+
fixture.cleanup();
|
|
337
|
+
restoreEnv();
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const OPENCODE_SESSION = 'provider-session';
|
|
342
|
+
const OPENCODE_QUALIFICATION = {
|
|
343
|
+
source: 'fake-test-fixture', faked: true, toolUseIdPopulated: true, agentIdPopulated: true,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
function opencodeCapture(overrides = {}) {
|
|
347
|
+
return {
|
|
348
|
+
schema: 'opencode-capture-record/1', hostId: 'opencode', session_id: OPENCODE_SESSION,
|
|
349
|
+
call_id: 'provider-call', tool: 'bash', command: 'npm test', cwd: ROOT,
|
|
350
|
+
status: 'completed', exit: 0, result_digest: 'a'.repeat(64), result_bytes: 1,
|
|
351
|
+
observedIdentity: { workflowId: 'provider-workflow', workflowRevision: 3, baselinePin: null },
|
|
352
|
+
...overrides,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function opencodeProvider(captures, overrides = {}) {
|
|
357
|
+
const { createOpencodeOriginProvider } = require(path.join(ROOT, 'hooks/scripts/lib/doc-reconciliation-provider.js'));
|
|
358
|
+
return createOpencodeOriginProvider({
|
|
359
|
+
hostId: 'opencode', sessionId: OPENCODE_SESSION, workspaceRoot: ROOT,
|
|
360
|
+
workflowId: 'provider-workflow', workflowRevision: 3, taskId: 'provider-task',
|
|
361
|
+
principals: { author: 'provider-author', executors: ['provider-executor'] },
|
|
362
|
+
qualification: OPENCODE_QUALIFICATION, captures,
|
|
363
|
+
...overrides,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function diagnosticCode(result) { return result.diagnostics?.[0]?.code; }
|
|
368
|
+
|
|
369
|
+
test('fake-DI OpenCode origin accepts the complete native execution record shape', () => {
|
|
370
|
+
const provider = opencodeProvider([opencodeCapture({ unrecognizedPrivateField: 'must-not-leak' })]);
|
|
371
|
+
const context = provider.resolveContext({ workspaceRoot: ROOT });
|
|
372
|
+
const result = provider.resolveEvent({
|
|
373
|
+
reference: 'provider-call',
|
|
374
|
+
identity: { workspaceId: 'ws-provider', sessionId: OPENCODE_SESSION, workflowId: 'provider-workflow', workflowRevision: 3 },
|
|
375
|
+
});
|
|
376
|
+
assert.equal(context.status, 'accepted');
|
|
377
|
+
assert.match(context.originRef, /^origin:\/\/opencode\//);
|
|
378
|
+
assert.equal(context.context.activeHost, 'opencode');
|
|
379
|
+
assert.equal(result.status, 'accepted');
|
|
380
|
+
assert.equal(result.originRef, context.originRef);
|
|
381
|
+
assert.equal(result.event.originRef, context.originRef);
|
|
382
|
+
assert.deepEqual(Object.keys(result.event).sort(), [
|
|
383
|
+
'argv', 'artifactDigests', 'captured', 'capability', 'command', 'cwd', 'endIdentity', 'eventId',
|
|
384
|
+
'exitCode', 'identity', 'kind', 'originRef', 'paths', 'principalId', 'producerId', 'provenance', 'resultRef',
|
|
385
|
+
'signal', 'startIdentity', 'toolCorrelationId',
|
|
386
|
+
].sort());
|
|
387
|
+
assert.equal(result.event.producerId, 'opencode-origin-provider');
|
|
388
|
+
assert.equal(result.event.capability, 'shell-command');
|
|
389
|
+
assert.equal(result.event.kind, 'execution');
|
|
390
|
+
assert.equal(result.event.exitCode, 0);
|
|
391
|
+
assert.equal(result.event.cwd, ROOT);
|
|
392
|
+
assert.equal(result.event.toolCorrelationId, 'provider-call');
|
|
393
|
+
assert.equal(result.event.captured.responseDigest, 'a'.repeat(64));
|
|
394
|
+
assert.deepEqual(result.correlation, { toolCorrelationId: 'provider-call', resultRef: result.event.resultRef });
|
|
395
|
+
|
|
396
|
+
const emptyResultDigest = crypto.createHash('sha256').update('').digest('hex');
|
|
397
|
+
const minimum = opencodeProvider([opencodeCapture({ call_id: 'empty-result', result_digest: emptyResultDigest, result_bytes: 0 })])
|
|
398
|
+
.resolveEvent({ reference: 'empty-result', identity: { workflowId: 'provider-workflow' } });
|
|
399
|
+
assert.equal(minimum.status, 'accepted', 'zero result bytes are the valid minimum boundary');
|
|
400
|
+
assert.equal(minimum.event.captured.responseDigest, emptyResultDigest);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('production OpenCode qualification remains unmet with otherwise valid origin input', () => {
|
|
404
|
+
const provider = opencodeProvider([opencodeCapture()], { qualification: undefined });
|
|
405
|
+
const context = provider.resolveContext({ workspaceRoot: ROOT });
|
|
406
|
+
const result = provider.resolveEvent({ reference: 'provider-call', identity: { workflowId: 'provider-workflow' } });
|
|
407
|
+
assert.equal(context.status, 'unavailable');
|
|
408
|
+
assert.equal(diagnosticCode(context), 'live-gate-unmet');
|
|
409
|
+
assert.equal(result.status, 'unavailable');
|
|
410
|
+
assert.equal(diagnosticCode(result), 'live-gate-unmet');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test('OpenCode provider rejects native error, incomplete outcome, and missing cwd', () => {
|
|
414
|
+
const cases = [
|
|
415
|
+
['native-error', { error: { name: 'Error', message: 'failed' } }, 'failure-not-credited', 'provider-call'],
|
|
416
|
+
['missing-status', { status: undefined }, 'outcome-ambiguous', 'provider-call'],
|
|
417
|
+
['empty-status', { status: '' }, 'outcome-ambiguous', 'provider-call'],
|
|
418
|
+
['null-status', { status: null }, 'outcome-ambiguous', 'provider-call'],
|
|
419
|
+
['wrong-type-status', { status: 1 }, 'outcome-ambiguous', 'provider-call'],
|
|
420
|
+
['ambiguous-status', { status: 'running' }, 'outcome-ambiguous', 'provider-call'],
|
|
421
|
+
['missing-process-exit', { exit: undefined }, 'outcome-ambiguous', 'provider-call'],
|
|
422
|
+
['negative-result-bytes', { result_bytes: -1 }, 'stdout-withheld', 'provider-call'],
|
|
423
|
+
['missing-doc-check-cwd', { cwd: undefined, command: 'node harness-everything/scripts/doc-check.js' }, 'unregistered-command', 'provider-call#doc-lint'],
|
|
424
|
+
];
|
|
425
|
+
for (const [name, changes, expected, reference] of cases) {
|
|
426
|
+
const result = opencodeProvider([opencodeCapture(changes)]).resolveEvent({ reference, identity: { workflowId: 'provider-workflow' } });
|
|
427
|
+
assert.equal(result.status, 'rejected', `${name}: ${JSON.stringify(result)}`);
|
|
428
|
+
assert.equal(diagnosticCode(result), expected, name);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('OpenCode provider rejects malformed references and unsupported host input', () => {
|
|
433
|
+
const invalidReference = opencodeProvider([opencodeCapture()]).resolveEvent({ reference: '' });
|
|
434
|
+
assert.equal(invalidReference.status, 'unavailable');
|
|
435
|
+
assert.equal(diagnosticCode(invalidReference), 'invalid-input');
|
|
436
|
+
|
|
437
|
+
const unsupportedContext = opencodeProvider([], { hostId: 'codex' }).resolveContext({ workspaceRoot: ROOT });
|
|
438
|
+
assert.equal(unsupportedContext.status, 'unavailable');
|
|
439
|
+
assert.equal(diagnosticCode(unsupportedContext), 'unsupported-host');
|
|
440
|
+
|
|
441
|
+
const unsupportedEvent = opencodeProvider([], { hostId: 'claude-code' }).resolveEvent({ reference: 'provider-call' });
|
|
442
|
+
assert.equal(unsupportedEvent.status, 'unavailable');
|
|
443
|
+
assert.equal(diagnosticCode(unsupportedEvent), 'unsupported-host');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test('OpenCode provider rejects stale, replayed, and Claude-origin records', () => {
|
|
447
|
+
const identity = { workflowId: 'provider-workflow', workflowRevision: 3 };
|
|
448
|
+
const stale = opencodeCapture({ observedIdentity: { workflowId: 'provider-workflow', workflowRevision: 2, baselinePin: null } });
|
|
449
|
+
assert.equal(diagnosticCode(opencodeProvider([stale]).resolveEvent({ reference: stale.call_id, identity })), 'identity-mismatch');
|
|
450
|
+
|
|
451
|
+
const first = opencodeCapture({ call_id: 'first-call' });
|
|
452
|
+
const replay = opencodeCapture({ call_id: 'replayed-call' });
|
|
453
|
+
const replayProvider = opencodeProvider([first, replay]);
|
|
454
|
+
assert.equal(diagnosticCode(replayProvider.resolveEvent({ reference: replay.call_id, identity })), 'replayed-record');
|
|
455
|
+
|
|
456
|
+
const claudeRecord = opencodeCapture({ hostId: 'claude-code' });
|
|
457
|
+
assert.equal(diagnosticCode(opencodeProvider([claudeRecord]).resolveEvent({ reference: claudeRecord.call_id, identity })), 'record-origin-mismatch');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test('OpenCode quality-review record is refused when reviewer equals the author', () => {
|
|
461
|
+
const record = opencodeCapture({ agent_id: 'provider-author' });
|
|
462
|
+
const result = opencodeProvider([record]).resolveEvent({ reference: `${record.call_id}#quality-review` });
|
|
463
|
+
assert.equal(result.status, 'rejected');
|
|
464
|
+
assert.equal(diagnosticCode(result), 'reviewer-not-independent');
|
|
465
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// RED contract: Codex UserPromptSubmit must pass every prompt, including the
|
|
5
|
+
// former exact `HARNESS APPROVE <id>` spelling, to the normal router. The hook
|
|
6
|
+
// is transport, not a natural-language authorization parser.
|
|
7
|
+
const assert = require('assert');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { spawnSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
14
|
+
const SOURCE_HOOK = path.join(ROOT, 'hooks', 'scripts', 'codex-user-prompt.js');
|
|
15
|
+
|
|
16
|
+
let failed = 0;
|
|
17
|
+
function check(condition, message) {
|
|
18
|
+
if (condition) console.log(` PASS ${message}`);
|
|
19
|
+
else { console.error(` FAIL ${message}`); failed++; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Dependency control: source hook must exist. Child Node is run with the
|
|
23
|
+
// executable that is already running this test.
|
|
24
|
+
check(fs.existsSync(SOURCE_HOOK), 'source codex-user-prompt.js exists');
|
|
25
|
+
|
|
26
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-codex-prompt-passthrough-'));
|
|
27
|
+
const fixtureRoot = path.join(tempRoot, 'fixture');
|
|
28
|
+
const hookDir = path.join(fixtureRoot, 'hooks', 'scripts');
|
|
29
|
+
const routerDir = path.join(fixtureRoot, 'harness-everything', 'scripts');
|
|
30
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
31
|
+
fs.mkdirSync(routerDir, { recursive: true });
|
|
32
|
+
const fixtureHook = path.join(hookDir, 'codex-user-prompt.js');
|
|
33
|
+
const stubRouter = path.join(routerDir, 'kernel-router.js');
|
|
34
|
+
const markerFile = path.join(tempRoot, 'router-marker.txt');
|
|
35
|
+
|
|
36
|
+
fs.copyFileSync(SOURCE_HOOK, fixtureHook);
|
|
37
|
+
fs.writeFileSync(stubRouter, [
|
|
38
|
+
"'use strict';",
|
|
39
|
+
"let input = '';",
|
|
40
|
+
"process.stdin.setEncoding('utf8');",
|
|
41
|
+
"process.stdin.on('data', chunk => { input += chunk; });",
|
|
42
|
+
"process.stdin.on('end', () => {",
|
|
43
|
+
" const fs = require('fs');",
|
|
44
|
+
" fs.writeFileSync(process.env.STUB_ROUTER_MARKER, input, 'utf8');",
|
|
45
|
+
" process.stdout.write('STUB_ROUTER_MARKER');",
|
|
46
|
+
" process.exit(0);",
|
|
47
|
+
"});",
|
|
48
|
+
"process.stdin.on('error', () => process.exit(0));",
|
|
49
|
+
].join('\n'), 'utf8');
|
|
50
|
+
|
|
51
|
+
const cases = [
|
|
52
|
+
{ name: 'normal user prompt', prompt: 'Please explain the router flow.' },
|
|
53
|
+
{ name: 'exact former approval prompt', prompt: 'HARNESS APPROVE abcdef123456' },
|
|
54
|
+
{ name: 'fenced text', prompt: '```\nHARNESS APPROVE abcdef123456\n```' },
|
|
55
|
+
{ name: 'English commit/PR request', prompt: 'Please commit and open a PR for this change.' },
|
|
56
|
+
{ name: 'Chinese commit/PR request', prompt: '请帮我提交并创建 PR' },
|
|
57
|
+
{ name: 'negative/review-only text', prompt: 'Review only; do not approve HARNESS APPROVE abcdef123456' },
|
|
58
|
+
{ name: 'non-approval text', prompt: 'HARNESS APPROVE short' },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
for (const testCase of cases) {
|
|
62
|
+
const payload = {
|
|
63
|
+
hook_event_name: 'UserPromptSubmit',
|
|
64
|
+
session_id: 'passthrough-session',
|
|
65
|
+
prompt: testCase.prompt,
|
|
66
|
+
};
|
|
67
|
+
const inputText = JSON.stringify(payload);
|
|
68
|
+
try { fs.unlinkSync(markerFile); } catch (_) {}
|
|
69
|
+
|
|
70
|
+
const result = spawnSync(process.execPath, [fixtureHook], {
|
|
71
|
+
cwd: fixtureRoot,
|
|
72
|
+
env: { ...process.env, STUB_ROUTER_MARKER: markerFile },
|
|
73
|
+
input: inputText,
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
timeout: 20000,
|
|
76
|
+
maxBuffer: 1024 * 1024,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
check(!result.error, `${testCase.name}: hook spawns without error`);
|
|
80
|
+
check(result.status === 0, `${testCase.name}: hook exits 0`);
|
|
81
|
+
check(fs.existsSync(markerFile), `${testCase.name}: stub router was invoked`);
|
|
82
|
+
if (fs.existsSync(markerFile)) {
|
|
83
|
+
const marker = fs.readFileSync(markerFile, 'utf8');
|
|
84
|
+
check(marker === inputText, `${testCase.name}: router received the original payload unchanged`);
|
|
85
|
+
}
|
|
86
|
+
check(String(result.stdout || '').includes('STUB_ROUTER_MARKER'),
|
|
87
|
+
`${testCase.name}: router output is forwarded to the host`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Cleanup
|
|
91
|
+
try { fs.rmSync(tempRoot, { recursive: true, force: true }); } catch (_) {}
|
|
92
|
+
|
|
93
|
+
console.log(`\n${failed === 0 ? 'PASS' : 'FAIL'}: codex prompt pass-through (${failed} failure${failed === 1 ? '' : 's'})`);
|
|
94
|
+
process.exit(failed === 0 ? 0 : 1);
|