harness-everything 0.41.3 → 0.42.1
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 +18 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/README.md +68 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/green-final.log +162 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/green.log +158 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/live-installed-runtime-rejection.json +31 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-action-family-cross-product.js +143 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-action-family-cross-product.log +7 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-codex-action-gate-prehook-smoke.js +78 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-codex-action-gate-prehook-smoke.log +8 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/luna-review.md +151 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/original-rejection.json +31 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/red.log +158 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-descriptor-green.log +192 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-descriptor-red.log +192 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-family-green.log +206 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-family-red.log +206 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-hints-green.log +174 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/review-hints-red.log +174 -0
- package/benchmarks/results/codex-pr-metadata-2026-10-26/verification.md +31 -0
- package/benchmarks/results/doc-evidence-e1/README.md +68 -0
- package/benchmarks/results/doc-evidence-e1/evidence.json +360 -0
- package/benchmarks/results/doc-evidence-e1/pilot-report.json +175 -0
- package/benchmarks/results/h1-provider-independence-qualification/README.md +95 -0
- package/benchmarks/results/live-host/opencode-d1-d2-2026-10-25/README.md +30 -0
- package/benchmarks/results/live-host/opencode-d1-d2-2026-10-25/evidence.json +128 -0
- package/ci/action-gate.test.js +86 -0
- package/ci/mechanism-2aa-codex-action-gate-policy.test.js +47 -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-task-git-authorization.test.js +12 -0
- package/docs/codex-action-gate.md +27 -1
- package/docs/doc-workflow-reconciliation.md +4 -3
- package/docs/document-lifecycle.md +3 -3
- package/docs/platform-capabilities.md +8 -2
- package/hooks/scripts/action-gate.js +35 -8
- 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 +35 -8
- 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
|
@@ -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
|
+
});
|
|
@@ -131,6 +131,18 @@ try {
|
|
|
131
131
|
}));
|
|
132
132
|
assert.strictEqual(neutralResult.status, 0, 'exact create_pull_request operation must be host-deferred on Codex (currently exits 2: RED)');
|
|
133
133
|
|
|
134
|
+
for (const tool of ['mcp__codex_apps__github__update_pull_request', 'mcp__codex_apps__github_update_pull_request', 'mcp__github__update_pull_request']) {
|
|
135
|
+
for (const fields of [{ body: 'replacement body' }, { title: 'replacement title' }, { title: 'both', body: '' }]) {
|
|
136
|
+
const result = run(payload(tool, { repository_full_name: 'acme/repo', pr_number: 1, ...fields }));
|
|
137
|
+
assert.strictEqual(result.status, 0, 'metadata-only actual stdin payload must be host-deferred');
|
|
138
|
+
assert.strictEqual(result.stdout.trim(), '', 'metadata deferral must emit no permission decision');
|
|
139
|
+
}
|
|
140
|
+
const denied = run(payload(tool, { repository_full_name: 'acme/repo', pr_number: 1, body: 'mixed', state: 'closed' }));
|
|
141
|
+
assert.strictEqual(denied.status, 2, 'mixed metadata/state actual stdin payload must fail closed');
|
|
142
|
+
assert.strictEqual(denied.stdout.trim(), '');
|
|
143
|
+
assert.match(denied.stderr, /structured-mutate/);
|
|
144
|
+
}
|
|
145
|
+
|
|
134
146
|
// Genuine mutations remain blocked, but the fixed-format approval protocol is retired.
|
|
135
147
|
const legacyMutation = run(payload('mcp__demo__update_issue', { issue_id: 1, title: 'changed' }));
|
|
136
148
|
assert.strictEqual(legacyMutation.status, 2, 'genuine mutation must remain blocked on Codex');
|
|
@@ -132,7 +132,8 @@ Harness does not parse, bind, or intercept any fixed approval syntax in user
|
|
|
132
132
|
prompts. Native agent task-scope authorization and native permissions remain
|
|
133
133
|
authoritative.
|
|
134
134
|
|
|
135
|
-
Routine exact `create_pull_request` operations
|
|
135
|
+
Routine exact `create_pull_request` operations and Codex title/body-only
|
|
136
|
+
`update_pull_request` operations are host-deferred (below). All
|
|
136
137
|
other risky actions (merge/delete/deploy/publish/send/force-push and unrelated
|
|
137
138
|
genuine mutations) remain hard PreToolUse blocks; an explicitly required
|
|
138
139
|
`actionGate` hint still gates. These are deterministic mechanism changes, not
|
|
@@ -153,3 +154,28 @@ risky actions (merge/delete/deploy/publish/send/force-push and unrelated genuine
|
|
|
153
154
|
mutations) remain gated; exact operation matching is used, with no substring
|
|
154
155
|
exemption, so misleading names such as `create_pull_requests` or
|
|
155
156
|
`foo_create_pull_request` still block.
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
## Codex PR title/body updates (host-deferred)
|
|
160
|
+
|
|
161
|
+
The Codex-only metadata exception checks the actual tool input, with all of these conditions:
|
|
162
|
+
|
|
163
|
+
- Exact final `__` operation `update_pull_request`, or exact flattened connector name `mcp__codex_apps__github_update_pull_request`.
|
|
164
|
+
- A non-array object containing at least one string `title` or `body` (empty body is supported).
|
|
165
|
+
- Only `title`, `body`, and selector keys `repository_full_name`, `pr_number`, `owner`, `repo`, `pullNumber`, `pull_number` are present.
|
|
166
|
+
- Exact Codex PR updates cannot be downgraded to `read`/`unknown` by an explicit descriptor or a provider namespace; higher-risk action descriptors remain gated. Their tool family comes from the actual tool identity, so a descriptor cannot disguise the MCP update as shell input.
|
|
167
|
+
- No explicit required `actionGate` hint in any supported envelope (a required hint wins over conflicting optional hints); the action descriptor remains `mutate`/`gated`.
|
|
168
|
+
|
|
169
|
+
Any additional key, including `state`, `base_branch`, `base`, `maintainer_can_modify`, `draft`, or an unknown key, prevents the exception, even when its value is null. Mixed metadata/high-risk inputs and similar operation names retain the existing Gate. Claude, OpenCode and unknown hosts retain their prior mutation rules. PR creation behavior is unchanged.
|
|
170
|
+
|
|
171
|
+
```mermaid
|
|
172
|
+
flowchart LR
|
|
173
|
+
A["Exact PR update + actual arguments"] --> B{"Codex + title/body only<br/>+ no required Gate?"}
|
|
174
|
+
B -->|Yes| C["Host-deferred mutation<br/>no permission decision"]
|
|
175
|
+
B -->|No| D["Existing structured Gate"]
|
|
176
|
+
C --> E["Native task scope + permissions<br/>+ GitHub authorization"]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Host-deferred does not grant authorization. Harness emits no `permissionDecision` allow/ask; native permissions, task scope, and GitHub access still decide execution. The audit records `routine-pr-metadata-update`, `deferred-to-host`, host/tool provenance and the exact payload hash, without storing title/body. PostToolUse records execution success/failure; Stop rejects an unexecuted deferral. A changed payload cannot reuse the original hash.
|
|
180
|
+
|
|
181
|
+
Evidence: [RED/GREEN and independent review](../benchmarks/results/codex-pr-metadata-2026-10-26/README.md). Package/mechanism verification is separate from live Host verification (#110, #122, #135; affected PR #376).
|
|
@@ -6,7 +6,7 @@ status: active
|
|
|
6
6
|
# Document workflow reconciliation — Phase 3b S1 runtime
|
|
7
7
|
|
|
8
8
|
**Status: the runtime ships default OFF; opt in with `HARNESS_DOC_RECONCILIATION_PILOT=1`.
|
|
9
|
-
With the pilot ON, F4 ([#358](https://github.com/dyphn1/Harness-everything/pull/358)) grants limited positive credit for exactly `doc-lint` and `link-check`, from a host-captured run of the installed doc-check on Claude Code 2.1.309 (live runs 4–5). S3 provider integration is pending; E1
|
|
9
|
+
With the pilot ON, F4 ([#358](https://github.com/dyphn1/Harness-everything/pull/358)) grants limited positive credit for exactly `doc-lint` and `link-check`, from a host-captured run of the installed doc-check on Claude Code 2.1.309 (live runs 4–5). S3 provider integration is pending; E1 remains OPEN with insufficient qualifying exposure. The recorded OpenCode [D1/D2 attempts](../benchmarks/results/live-host/opencode-d1-d2-2026-10-25/README.md) do not qualify as E1 cycles.**
|
|
10
10
|
This document refines the [document lifecycle contract](document-lifecycle.md#phase-3b--narrow-integration-contract-s1-default-off-f4-limited-credit).
|
|
11
11
|
The [accepted E0](https://github.com/dyphn1/Harness-everything/issues/341#issuecomment-6308604128)
|
|
12
12
|
permits this limited, reversible consumer under the
|
|
@@ -336,7 +336,7 @@ Preparation assertion rows do not carry `reportOrigin`; the S1 runtime report ca
|
|
|
336
336
|
marker is never presented as a production report.
|
|
337
337
|
## S1 — switch, trusted scope and fail-closed no-credit gate
|
|
338
338
|
|
|
339
|
-
**S1 runtime ships default OFF (`HARNESS_DOC_RECONCILIATION_PILOT=1` opts in). With the pilot ON, F4 (#358) grants limited positive credit for `doc-lint` and `link-check` only; S3 provider integration is pending and
|
|
339
|
+
**S1 runtime ships default OFF (`HARNESS_DOC_RECONCILIATION_PILOT=1` opts in). With the pilot ON, F4 (#358) grants limited positive credit for `doc-lint` and `link-check` only; S3 provider integration is pending; E1 remains OPEN with insufficient qualifying exposure, and D1/D2 are non-qualifying.**
|
|
340
340
|
The [accepted E0](https://github.com/dyphn1/Harness-everything/issues/341#issuecomment-6308604128)
|
|
341
341
|
permits this limited implementation; the [posted execution plan](https://github.com/dyphn1/Harness-everything/issues/325)
|
|
342
342
|
orders Docs → RED → GREEN → independent review → green CI for each slice. S1 was the temporary
|
|
@@ -1085,7 +1085,8 @@ provider adapter remains pending. Require every expected applicable MUST and
|
|
|
1085
1085
|
freshness check before positive credit, plus source/package compatibility, negative cases and
|
|
1086
1086
|
relevant green CI including Windows. The S1 suites establish their tested mechanism/package
|
|
1087
1087
|
contracts; they do not establish independent host provenance, live-host loading or architectural
|
|
1088
|
-
benefit. E1
|
|
1088
|
+
benefit. E1 observation remains OPEN with insufficient qualifying exposure; it begins only after
|
|
1089
|
+
a creditable S2, and the D1/D2 attempts are non-qualifying. E1 GO and H1–H4/paired evidence
|
|
1089
1090
|
remain separate gates.
|
|
1090
1091
|
|
|
1091
1092
|
## F4: credited reconciliation (Claude-only, pilot ON)
|
|
@@ -4,7 +4,7 @@ Contract for the documentation family: project docs configuration (D5), document
|
|
|
4
4
|
|
|
5
5
|
Documentation is a **consumer** of the #320 workflow/obligation abstractions. Nothing here is a second router, a standing maintenance loop or a cognitive lock: every check is a pure function that reports diagnostics.
|
|
6
6
|
|
|
7
|
-
## Scope of this slice (status
|
|
7
|
+
## Scope of this slice (original 2026-10-20 checkpoint; Phase 3b/3c status updated 2026-10-25)
|
|
8
8
|
|
|
9
9
|
This table distinguishes merged mechanism code from end-to-end workflow integration. The last reviewed `main` commit is `29b1d65b398e2622af97bf067078373b7fa76b26` (v0.36.2). That release commit carries `[skip ci]` and only bumps package/distribution versions on top of `e5d22584e243d5b5e587ee0279625e97a9033160`, whose full-gate `CI` run [39374474269](https://github.com/dyphn1/Harness-everything/actions/runs/39374474269) completed with every job succeeded. The historical Windows System One resident teardown (`S1-RS15`) was observed in attempt 1 of [run 39175240370](https://github.com/dyphn1/Harness-everything/actions/runs/39175240370) on `main@75e04359ae5bf11a41739f8eb04d25a76ee8f3a7`; that run completed successfully on attempt 2 and says nothing about `29b1d65`. Treat the resident teardown as a known platform limitation only when a current run on the exact SHA reproduces it, and require an all-green full-gate run for that SHA before calling any baseline green.
|
|
10
10
|
|
|
@@ -15,8 +15,8 @@ This table distinguishes merged mechanism code from end-to-end workflow integrat
|
|
|
15
15
|
| 2 — roles, IDs, aliases, legacy, supersession, index, D6.9 PRD revisions | **Merged** in #327; do not rebuild this lifecycle logic for Phase 3 |
|
|
16
16
|
| Step-type prerequisite (#326) | **Complete:** PR #328 merged; Harness-owned `doc-change` is explicitly declared, has no implicit TDD binding, and is not auto-inserted by router/System One |
|
|
17
17
|
| 3a — document facts, obligations and evidence producer | **Merged** in #334: `harness-everything/scripts/doc-obligations.js` and `doc-evidence.js`. H3/H4 have *partial deterministic mechanism* coverage, not integrated workflow evidence. Known limitations: caller-attested envelope; `configDigest` presence-only; `candidateDigest` restricted to `docs/prd/`; missing independent task-scope binding. Deleted and old rename paths are bound to trusted per-path baseline evidence ([doc-evidence-baseline-binding.md](doc-evidence-baseline-binding.md), #335 fixed by #342), with workflow-disposition reconciliation left out of scope |
|
|
18
|
-
| 3b — pilot-local provider suggestions and workflow evidence reconciliation | **S1 runtime ships default OFF** (opt in with `HARNESS_DOC_RECONCILIATION_PILOT=1`), with trusted scope assessment and fail-closed withholding. **F4 (#358) adds limited positive credit** for exactly `doc-lint` and `link-check` from a host-captured run of the installed doc-check (Claude Code only; live runs on 2.1.309). See [S1 switch, trusted scope and no-credit contract](doc-workflow-reconciliation.md#s1--switch-trusted-scope-and-fail-closed-no-credit-gate). S3 pure advisory provider suggestions shipped in 0.37.0 (not wired); E1
|
|
19
|
-
| 3c — H1–H4 and paired-task evaluation | **
|
|
18
|
+
| 3b — pilot-local provider suggestions and workflow evidence reconciliation | **S1 runtime ships default OFF** (opt in with `HARNESS_DOC_RECONCILIATION_PILOT=1`), with trusted scope assessment and fail-closed withholding. **F4 (#358) adds limited positive credit** for exactly `doc-lint` and `link-check` from a host-captured run of the installed doc-check (Claude Code only; live runs on 2.1.309). See [S1 switch, trusted scope and no-credit contract](doc-workflow-reconciliation.md#s1--switch-trusted-scope-and-fail-closed-no-credit-gate). S3 pure advisory provider suggestions shipped in 0.37.0 (not wired); E1 remains OPEN with insufficient qualifying exposure, and D1/D2 are non-qualifying. Accepted #341 E0 permits this limited reversible consumer; the live runs are not E1 GO; preserve existing required-binding compatibility |
|
|
19
|
+
| 3c — H1–H4 and paired-task evaluation | **H1 preparation only** in [#373](https://github.com/dyphn1/Harness-everything/pull/373) ([retained evidence](../benchmarks/results/h1-provider-independence-qualification/README.md)); full H1–H4 and paired-task evaluation remain pending. Mechanism tests alone are insufficient for architecture promotion; compare frozen baseline and treatment on real bounded tasks, success/failure first, then conditional cost and latency |
|
|
20
20
|
| 1b — shared documentation craft and family metadata | **Shared craft delivered** in F2 (#356): shared references with thin skill consumers. Family metadata is not established. A small optional logical-family metadata seam may be piloted; physical skill directory moves remain blocked on #320 identity/path separation |
|
|
21
21
|
| 4 — derived-view generators and `describes:` drift | **Regenerate-equals delivered** in F3 (#357) for authorized generators; `describes:` drift reminders and broader maintenance are not established. Preserve generator authorization, deterministic regenerate-equals and scoped reminder semantics |
|
|
22
22
|
|
|
@@ -17,6 +17,10 @@ Statuses are intentionally explicit:
|
|
|
17
17
|
|
|
18
18
|
Codex and OpenCode are `Partial` in `liveHostVerification`. The Codex artifact verifies user-hook compatibility capture for two consecutive turns on one Windows Codex CLI host; native plugin-hook mounting and other host/version combinations remain unverified. OpenCode's 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
|
+
### Codex PR metadata action-gate boundary
|
|
21
|
+
|
|
22
|
+
Exact title/body-only `update_pull_request` calls are Codex-only host-deferred mutations after an argument allowlist check; explicit required Gate hints and all additional fields retain the existing Gate. Native task scope, permissions and GitHub authorization remain authoritative. Canonical/package mechanism tests cover hashes, audit provenance, outcomes and negative controls; this does not establish candidate live-host coverage. See [Codex policy](codex-action-gate.md#codex-pr-titlebody-updates-host-deferred).
|
|
23
|
+
|
|
20
24
|
### Routing contract vs. host evidence
|
|
21
25
|
|
|
22
26
|
The workflow plan controller rejects present non-array required/optional binding declarations before recording a requirement plan. This input-validation contract is covered by deterministic mechanism tests and does not change live-host evidence status.
|
|
@@ -27,7 +31,8 @@ This is a **repository/agent contract**, not an automatic upgrade to any platfor
|
|
|
27
31
|
|
|
28
32
|
### Action-gate mechanism vs. host semantic boundary
|
|
29
33
|
|
|
30
|
-
Routine exact `create_pull_request` operations
|
|
34
|
+
Routine exact `create_pull_request` operations and Codex title/body-only
|
|
35
|
+
`update_pull_request` operations are host-deferred at the mechanism level:
|
|
31
36
|
their descriptor remains a mutation (not read/safe), and no Harness consent or
|
|
32
37
|
`permissionDecision` is emitted. Native agent task-scope authorization and native
|
|
33
38
|
permissions stay authoritative; an explicit required `actionGate` hint still gates.
|
|
@@ -140,7 +145,8 @@ The public OpenAI Skills-only plugin is narrower again. Its ZIP contains the pac
|
|
|
140
145
|
Codex `UserPromptSubmit` prompts pass through to the normal router unchanged;
|
|
141
146
|
Harness does not parse or intercept a fixed approval syntax. Native agent
|
|
142
147
|
task-scope authorization and native permissions remain authoritative. Routine
|
|
143
|
-
exact `create_pull_request` operations
|
|
148
|
+
exact `create_pull_request` operations and Codex title/body-only
|
|
149
|
+
`update_pull_request` operations are host-deferred at the mechanism level:
|
|
144
150
|
their descriptor remains a mutation (not read/safe), and no Harness consent or
|
|
145
151
|
`permissionDecision` is emitted. All other risky actions remain hard PreToolUse
|
|
146
152
|
blocks; an explicitly required `actionGate` hint still gates. These are
|