harness-everything 0.37.0 → 0.38.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 +16 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/cases.json +70 -11
- package/ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json +24 -556
- package/ci/fixtures/doc-obligations/reconciliation-s1/cases.json +6842 -0
- package/ci/fixtures/doc-obligations/reconciliation-s1/fail-session-writes.js +116 -0
- package/ci/lib/doc-reconciliation-prep-fixtures.js +116 -10
- package/ci/mechanism-325-3b-preparation.test.js +8 -8
- package/ci/mechanism-325-3b-s1-review.test.js +831 -0
- package/ci/mechanism-325-3b-s1.test.js +188 -0
- package/ci/prep/doc-reconciliation-red.js +2 -1
- package/docs/doc-workflow-reconciliation.md +217 -26
- package/docs/document-lifecycle.md +4 -4
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-lifecycle.js +1 -1
- package/hooks/scripts/lib/doc-reconciliation-pilot.js +152 -0
- package/hooks/scripts/lib/workflow-obligations.js +44 -0
- package/hooks/scripts/workflow-disposition.js +2 -0
- package/hooks/scripts/workflow-stop-gate.js +24 -1
- 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/lib/doc-reconciliation-pilot.js +152 -0
- package/plugins/harness-everything/hooks/scripts/lib/workflow-obligations.js +44 -0
- package/plugins/harness-everything/hooks/scripts/workflow-disposition.js +2 -0
- package/plugins/harness-everything/hooks/scripts/workflow-stop-gate.js +24 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +1 -1
- package/sbom.cdx.json +7 -7
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// #325 PR #351 review follow-up ("Unwritable session store"): test-only preload that simulates an
|
|
4
|
+
// unwritable session store for one child process, without chmod (cross-platform).
|
|
5
|
+
//
|
|
6
|
+
// Loaded through NODE_OPTIONS=--require <this file> onto the real Stop entry point by
|
|
7
|
+
// ci/mechanism-325-3b-s1-review.test.js. The integration test passes the target session directory in the
|
|
8
|
+
// HARNESS_S1_REVIEW_SESSION_DIR environment variable. While that variable names a directory, every
|
|
9
|
+
// write/rename/open-for-write/unlink whose target is inside it fails with EACCES and nothing else is
|
|
10
|
+
// intercepted: reads, mkdir, and writes outside the session directory keep working, so a failed write
|
|
11
|
+
// cannot be confused with a skipped or missing one. Without the variable this preload is inert.
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
(function install() {
|
|
17
|
+
const sessionDirectory = process.env.HARNESS_S1_REVIEW_SESSION_DIR;
|
|
18
|
+
if (!sessionDirectory) return;
|
|
19
|
+
let resolvedDirectory;
|
|
20
|
+
try { resolvedDirectory = path.resolve(sessionDirectory); } catch (_) { return; }
|
|
21
|
+
if (!resolvedDirectory || resolvedDirectory === '/') return;
|
|
22
|
+
|
|
23
|
+
function unwritable(target) {
|
|
24
|
+
if (target === undefined || target === null) return false;
|
|
25
|
+
try {
|
|
26
|
+
const resolved = path.resolve(String(target));
|
|
27
|
+
const relative = path.relative(resolvedDirectory, resolved);
|
|
28
|
+
return relative === '' || (!relative.startsWith('..' + path.sep) && relative !== '..' && !path.isAbsolute(relative));
|
|
29
|
+
} catch (_) { return false; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeTargetOpenFlags(flags) {
|
|
33
|
+
// open()/openSync() only fail for write-capable flags; 'r' stays readable.
|
|
34
|
+
const value = String(flags || 'r');
|
|
35
|
+
return value.startsWith('w') || value.startsWith('a') || value.includes('+') || value.includes('x');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function eacces(method, target) {
|
|
39
|
+
const error = new Error(`EACCES: permission denied, ${method} ${target} (simulated session store write failure)`);
|
|
40
|
+
error.code = 'EACCES';
|
|
41
|
+
error.errno = -13;
|
|
42
|
+
error.syscall = method;
|
|
43
|
+
error.path = String(target);
|
|
44
|
+
return error;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const originalWriteFileSync = fs.writeFileSync;
|
|
48
|
+
fs.writeFileSync = function (file, ...rest) {
|
|
49
|
+
if (unwritable(file)) throw eacces('write', file);
|
|
50
|
+
return originalWriteFileSync.call(fs, file, ...rest);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const originalRenameSync = fs.renameSync;
|
|
54
|
+
fs.renameSync = function (from, to, ...rest) {
|
|
55
|
+
if (unwritable(from) || unwritable(to)) throw eacces('rename', from);
|
|
56
|
+
return originalRenameSync.call(fs, from, to, ...rest);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const originalOpenSync = fs.openSync;
|
|
60
|
+
fs.openSync = function (file, flags, ...rest) {
|
|
61
|
+
if (unwritable(file) && writeTargetOpenFlags(flags)) throw eacces('open', file);
|
|
62
|
+
return originalOpenSync.call(fs, file, flags, ...rest);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const originalUnlinkSync = fs.unlinkSync;
|
|
66
|
+
fs.unlinkSync = function (file, ...rest) {
|
|
67
|
+
if (unwritable(file)) throw eacces('unlink', file);
|
|
68
|
+
return originalUnlinkSync.call(fs, file, ...rest);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Callback forms used by async production paths, if any.
|
|
72
|
+
const normalizeCallback = args => (typeof args[args.length - 1] === 'function' ? args[args.length - 1] : () => {});
|
|
73
|
+
const originalWriteFile = fs.writeFile;
|
|
74
|
+
fs.writeFile = function (file, data, ...rest) {
|
|
75
|
+
const callback = normalizeCallback(rest);
|
|
76
|
+
if (unwritable(file)) return process.nextTick(() => callback(eacces('write', file)));
|
|
77
|
+
return originalWriteFile.call(fs, file, data, ...rest);
|
|
78
|
+
};
|
|
79
|
+
const originalRename = fs.rename;
|
|
80
|
+
fs.rename = function (from, to, ...rest) {
|
|
81
|
+
const callback = normalizeCallback(rest);
|
|
82
|
+
if (unwritable(from) || unwritable(to)) return process.nextTick(() => callback(eacces('rename', from)));
|
|
83
|
+
return originalRename.call(fs, from, to, ...rest);
|
|
84
|
+
};
|
|
85
|
+
const originalUnlink = fs.unlink;
|
|
86
|
+
fs.unlink = function (file, ...rest) {
|
|
87
|
+
const callback = normalizeCallback(rest);
|
|
88
|
+
if (unwritable(file)) return process.nextTick(() => callback(eacces('unlink', file)));
|
|
89
|
+
return originalUnlink.call(fs, file, ...rest);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// fs.promises equivalents, if any production path uses them.
|
|
93
|
+
const promises = fs.promises || null;
|
|
94
|
+
if (promises) {
|
|
95
|
+
const originalPromisesWriteFile = promises.writeFile;
|
|
96
|
+
promises.writeFile = function (file, data, ...rest) {
|
|
97
|
+
if (unwritable(file)) return Promise.reject(eacces('write', file));
|
|
98
|
+
return originalPromisesWriteFile.call(promises, file, data, ...rest);
|
|
99
|
+
};
|
|
100
|
+
const originalPromisesRename = promises.rename;
|
|
101
|
+
promises.rename = function (from, to, ...rest) {
|
|
102
|
+
if (unwritable(from) || unwritable(to)) return Promise.reject(eacces('rename', from));
|
|
103
|
+
return originalPromisesRename.call(promises, from, to, ...rest);
|
|
104
|
+
};
|
|
105
|
+
const originalPromisesUnlink = promises.unlink;
|
|
106
|
+
promises.unlink = function (file, ...rest) {
|
|
107
|
+
if (unwritable(file)) return Promise.reject(eacces('unlink', file));
|
|
108
|
+
return originalPromisesUnlink.call(promises, file, ...rest);
|
|
109
|
+
};
|
|
110
|
+
const originalPromisesOpen = promises.open;
|
|
111
|
+
promises.open = function (file, flags, ...rest) {
|
|
112
|
+
if (unwritable(file) && writeTargetOpenFlags(flags)) return Promise.reject(eacces('open', file));
|
|
113
|
+
return originalPromisesOpen.call(promises, file, flags, ...rest);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
})();
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
// Each replay creates a fresh workspace and state home, runs ordered actions through one explicitly
|
|
6
6
|
// selected distribution, and returns a replay root with the L1 shape:
|
|
7
7
|
// { actions: { [actionId]: { exitCode, signal, error, errorCode, stdout, stderr, json, state, pilot } }, state, pilot, pilotReportOrigin }.
|
|
8
|
-
// Nothing here is production code. The pilot switch is passed to children
|
|
9
|
-
//
|
|
8
|
+
// Nothing here is production code. The pilot switch is passed to children through their environment; the S1
|
|
9
|
+
// runtime reads it and, when ON, runs the fail-closed no-credit gate. Fixture operations
|
|
10
10
|
// (provideEvidence, mutateIdentity, timestampHints) write simulated records only inside the replay's own
|
|
11
|
-
// temporary directories. They are contract fixtures, not production commands
|
|
11
|
+
// temporary directories. They are contract fixtures, not production commands; no S1 runtime reads them.
|
|
12
|
+
// seedState writes one allowlisted session file (workflow-stages.json) into the replay's own state home, so a case can
|
|
13
|
+
// reach a correlated Fable stage contract. It is test-only; no production code reads or writes through it.
|
|
12
14
|
|
|
13
15
|
const crypto = require('crypto');
|
|
14
16
|
const fs = require('fs');
|
|
@@ -18,9 +20,16 @@ const { spawnSync } = require('child_process');
|
|
|
18
20
|
|
|
19
21
|
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
20
22
|
const PILOT_SWITCH = 'HARNESS_DOC_RECONCILIATION_PILOT';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
23
|
+
// Session-directory file the S1 runtime writes its pilot report to. Read-only here; only a report carrying
|
|
24
|
+
// reportOrigin: runtime is accepted.
|
|
25
|
+
const PILOT_REPORT_FILE = 'doc-reconciliation-pilot.json';
|
|
26
|
+
const BASELINE_AUTHOR = Object.freeze({ name: 'doc-recon-replay', email: 'doc-recon-replay@invalid' });
|
|
27
|
+
const BASELINE_DATE = '2026-01-01T00:00:00Z';
|
|
28
|
+
const FIXTURE_OPS = new Set(['provideEvidence', 'mutateIdentity', 'timestampHints', 'seedState', 'gitCommit']);
|
|
29
|
+
const CONTROLLER_OPS = new Set(['plan', 'start', 'binding', 'stageBinding', 'step', 'obligation']);
|
|
30
|
+
const ALL_OPS = new Set(['route', 'stop', 'stopGeneric', ...CONTROLLER_OPS, ...FIXTURE_OPS]);
|
|
31
|
+
// The only session files a case may seed, and only inside its own temporary state home.
|
|
32
|
+
const SEEDABLE_FILES = new Set(['workflow-stages.json']);
|
|
24
33
|
|
|
25
34
|
// Explicit distributions. Every entry point is named by absolute path; no module is reused across legs.
|
|
26
35
|
const DISTRIBUTIONS = Object.freeze({
|
|
@@ -28,13 +37,15 @@ const DISTRIBUTIONS = Object.freeze({
|
|
|
28
37
|
root: REPO_ROOT,
|
|
29
38
|
controller: path.join(REPO_ROOT, 'hooks', 'scripts', 'workflow-disposition.js'),
|
|
30
39
|
stopGate: path.join(REPO_ROOT, 'hooks', 'scripts', 'workflow-stop-gate.js'),
|
|
31
|
-
kernel: path.join(REPO_ROOT, 'harness-everything', 'scripts', 'kernel-router.js')
|
|
40
|
+
kernel: path.join(REPO_ROOT, 'harness-everything', 'scripts', 'kernel-router.js'),
|
|
41
|
+
genericStopGate: path.join(REPO_ROOT, 'hooks', 'scripts', 'stop-gate.js')
|
|
32
42
|
},
|
|
33
43
|
packaged: {
|
|
34
44
|
root: path.join(REPO_ROOT, 'plugins', 'harness-everything'),
|
|
35
45
|
controller: path.join(REPO_ROOT, 'plugins', 'harness-everything', 'hooks', 'scripts', 'workflow-disposition.js'),
|
|
36
46
|
stopGate: path.join(REPO_ROOT, 'plugins', 'harness-everything', 'hooks', 'scripts', 'workflow-stop-gate.js'),
|
|
37
|
-
kernel: path.join(REPO_ROOT, 'plugins', 'harness-everything', 'skills', 'harness-everything', 'scripts', 'kernel-router.js')
|
|
47
|
+
kernel: path.join(REPO_ROOT, 'plugins', 'harness-everything', 'skills', 'harness-everything', 'scripts', 'kernel-router.js'),
|
|
48
|
+
genericStopGate: path.join(REPO_ROOT, 'plugins', 'harness-everything', 'hooks', 'scripts', 'stop-gate.js')
|
|
38
49
|
}
|
|
39
50
|
});
|
|
40
51
|
|
|
@@ -196,6 +207,29 @@ function runFixture(handle, action, pilot) {
|
|
|
196
207
|
fs.writeFileSync(target, String(p.content), 'utf8');
|
|
197
208
|
return fixtureResult(handle, pilot, { op: 'mutateIdentity', kind: p.kind, path: p.path });
|
|
198
209
|
}
|
|
210
|
+
if (action.op === 'gitCommit') {
|
|
211
|
+
// Commits the replay workspace's current tree into HEAD mid-workflow. The baseline recorded at plan is not moved by
|
|
212
|
+
// this fixture: the case exists to show that scope is still assessed against that recorded baseline. A clean tree
|
|
213
|
+
// makes `git commit` fail, so a misused op is an infrastructure fault, not a silent no-op.
|
|
214
|
+
if (typeof p.message !== 'string' || !p.message.trim()) throw new Error('gitCommit needs a message');
|
|
215
|
+
git(handle.workspace, ['add', '-A']);
|
|
216
|
+
git(handle.workspace, [
|
|
217
|
+
'-c', `user.name=${BASELINE_AUTHOR.name}`, '-c', `user.email=${BASELINE_AUTHOR.email}`, '-c', 'commit.gpgsign=false',
|
|
218
|
+
'commit', '-q', '--no-verify', '-m', p.message
|
|
219
|
+
]);
|
|
220
|
+
return fixtureResult(handle, pilot, { op: 'gitCommit' });
|
|
221
|
+
}
|
|
222
|
+
if (action.op === 'seedState') {
|
|
223
|
+
// Writes one allowlisted session file into the replay's own state home. The session must already exist, so the
|
|
224
|
+
// seeded file lands where the runtime reads it; nothing outside the replay's state home is reachable.
|
|
225
|
+
if (!SEEDABLE_FILES.has(p.file)) throw new Error(`seedState may only write ${[...SEEDABLE_FILES].join(', ')}`);
|
|
226
|
+
const dir = sessionDirOf(handle);
|
|
227
|
+
if (!dir) throw new Error('seedState needs an existing session');
|
|
228
|
+
const target = path.join(dir, p.file);
|
|
229
|
+
if (!target.startsWith(handle.stateHome + path.sep)) throw new Error('seedState target escaped the replay state home');
|
|
230
|
+
fs.writeFileSync(target, JSON.stringify(p.json, null, 2), 'utf8');
|
|
231
|
+
return fixtureResult(handle, pilot, { op: 'seedState', file: p.file });
|
|
232
|
+
}
|
|
199
233
|
if (action.op === 'timestampHints') {
|
|
200
234
|
const dir = sessionDirOf(handle);
|
|
201
235
|
if (!dir) throw new Error('timestampHints needs an existing session');
|
|
@@ -222,6 +256,9 @@ function runAction(handle, action, pilot) {
|
|
|
222
256
|
result = runScript(handle, scriptFor(handle, 'route', dist.kernel), [], { session_id: sid, cwd: handle.workspace, prompt: p.prompt }, pilot);
|
|
223
257
|
} else if (action.op === 'stop') {
|
|
224
258
|
result = runScript(handle, scriptFor(handle, 'stop', dist.stopGate), [], { session_id: sid, cwd: handle.workspace, hook_event_name: 'Stop' }, pilot);
|
|
259
|
+
} else if (action.op === 'stopGeneric') {
|
|
260
|
+
// The second Stop surface (verification reminder). It must exit 0 in every case, like the workflow surface.
|
|
261
|
+
result = runScript(handle, scriptFor(handle, 'stopGeneric', dist.genericStopGate), [], { session_id: sid, cwd: handle.workspace, hook_event_name: 'Stop' }, pilot);
|
|
225
262
|
} else if (action.op === 'plan') {
|
|
226
263
|
result = runScript(handle, scriptFor(handle, 'plan', dist.controller), [
|
|
227
264
|
'plan', '--session-id', sid, '--requirements-json', JSON.stringify(p.requirements),
|
|
@@ -234,6 +271,12 @@ function runAction(handle, action, pilot) {
|
|
|
234
271
|
'binding', '--session-id', sid, '--step-id', p.stepId, '--binding-id', p.bindingId,
|
|
235
272
|
'--disposition', p.disposition, '--evidence', p.evidence
|
|
236
273
|
], undefined, pilot);
|
|
274
|
+
} else if (action.op === 'stageBinding') {
|
|
275
|
+
// Correlated Fable stage binding (the stage-binding command). Generic binding refuses Fable workflows.
|
|
276
|
+
result = runScript(handle, scriptFor(handle, 'stageBinding', dist.controller), [
|
|
277
|
+
'stage-binding', '--session-id', sid, '--stage-id', p.stageId, '--binding-id', p.bindingId,
|
|
278
|
+
'--disposition', p.disposition, '--evidence', p.evidence
|
|
279
|
+
], undefined, pilot);
|
|
237
280
|
} else if (action.op === 'step') {
|
|
238
281
|
result = runScript(handle, scriptFor(handle, 'step', dist.controller), [
|
|
239
282
|
'step', '--session-id', sid, '--step-id', p.stepId, '--disposition', p.disposition, '--evidence', p.evidence
|
|
@@ -267,6 +310,69 @@ function effectivePilot(action, replayPilot) {
|
|
|
267
310
|
return Object.prototype.hasOwnProperty.call(action, 'pilot') ? action.pilot : replayPilot;
|
|
268
311
|
}
|
|
269
312
|
|
|
313
|
+
// Runs git in a replay workspace. Inherited GIT_* variables are removed, so a hook or CI environment cannot point git
|
|
314
|
+
// at another repository. Commit dates are fixed so identical inputs give the same baseline commit on both legs.
|
|
315
|
+
function git(cwd, args) {
|
|
316
|
+
const env = { ...process.env };
|
|
317
|
+
for (const key of Object.keys(env)) {
|
|
318
|
+
if (key.startsWith('GIT_')) delete env[key];
|
|
319
|
+
}
|
|
320
|
+
env.GIT_AUTHOR_DATE = BASELINE_DATE;
|
|
321
|
+
env.GIT_COMMITTER_DATE = BASELINE_DATE;
|
|
322
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8', env, timeout: CHILD_LIMITS.timeoutMs, maxBuffer: CHILD_LIMITS.maxBuffer });
|
|
323
|
+
if (result.error || result.status !== 0) {
|
|
324
|
+
const detail = result.error ? result.error.message : String(result.stderr || '').split('\n')[0];
|
|
325
|
+
throw new Error(`git failed in the replay workspace: ${detail}`);
|
|
326
|
+
}
|
|
327
|
+
return result.stdout;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function writeWorkspaceFiles(workspace, files) {
|
|
331
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
332
|
+
if (typeof rel !== 'string' || path.isAbsolute(rel) || rel.split(/[\\/]/).includes('..')) {
|
|
333
|
+
throw new Error(`input path must be workspace-relative: ${rel}`);
|
|
334
|
+
}
|
|
335
|
+
const target = path.join(workspace, rel);
|
|
336
|
+
if (content === null) {
|
|
337
|
+
fs.rmSync(target, { force: true });
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (typeof content !== 'string') throw new Error(`input file ${rel} must be a string or null`);
|
|
341
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
342
|
+
fs.writeFileSync(target, content, 'utf8');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Materialises a case's declared file inputs before any action runs. Non-empty baseFiles become a committed Git
|
|
347
|
+
// baseline, so `plan` records a real HEAD. candidateFiles then become the working tree, and a null value deletes a
|
|
348
|
+
// baseline file. Without baseFiles the workspace stays non-Git, so its baseline is unavailable. Fixture maps alone are
|
|
349
|
+
// not runtime authority; the runtime collects scope itself.
|
|
350
|
+
function materialiseInput(handle, input) {
|
|
351
|
+
const base = (input && input.baseFiles) || {};
|
|
352
|
+
const candidate = (input && input.candidateFiles) || {};
|
|
353
|
+
if (Object.keys(base).length) {
|
|
354
|
+
git(handle.workspace, ['init', '-q']);
|
|
355
|
+
writeWorkspaceFiles(handle.workspace, base);
|
|
356
|
+
git(handle.workspace, ['add', '-A']);
|
|
357
|
+
git(handle.workspace, [
|
|
358
|
+
'-c', `user.name=${BASELINE_AUTHOR.name}`, '-c', `user.email=${BASELINE_AUTHOR.email}`, '-c', 'commit.gpgsign=false',
|
|
359
|
+
'commit', '-q', '--no-verify', '-m', 'baseline'
|
|
360
|
+
]);
|
|
361
|
+
}
|
|
362
|
+
writeWorkspaceFiles(handle.workspace, candidate);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Read-only extraction of a runtime pilot report from the session directory. Only a report that carries
|
|
366
|
+
// reportOrigin: runtime is accepted. Otherwise the replay reports no pilot with origin harness; nothing is synthesized.
|
|
367
|
+
function runtimePilotOf(handle) {
|
|
368
|
+
const dir = sessionDirOf(handle);
|
|
369
|
+
const report = dir ? readJsonOrNull(path.join(dir, PILOT_REPORT_FILE)) : null;
|
|
370
|
+
if (report && typeof report === 'object' && !Array.isArray(report) && report.reportOrigin === 'runtime') {
|
|
371
|
+
return { pilot: report, pilotReportOrigin: 'runtime' };
|
|
372
|
+
}
|
|
373
|
+
return { pilot: null, pilotReportOrigin: 'harness' };
|
|
374
|
+
}
|
|
375
|
+
|
|
270
376
|
// Replays one case through one distribution. Actions run in order and the replay stops at its first infrastructure
|
|
271
377
|
// fault: the faulted action is recorded, no later action runs, and the temporary directories are still removed.
|
|
272
378
|
// A crash is recorded, not hidden.
|
|
@@ -275,6 +381,7 @@ function replayCase(caseDef, distribution, options = {}) {
|
|
|
275
381
|
const mode = options.pilot === undefined ? 'off' : 'on';
|
|
276
382
|
const handle = { ...createSession(distribution, caseDef.id, options.pilot), distribution, limits };
|
|
277
383
|
try {
|
|
384
|
+
materialiseInput(handle, caseDef.input);
|
|
278
385
|
const actions = {};
|
|
279
386
|
for (const action of caseDef.actions) {
|
|
280
387
|
if (Object.prototype.hasOwnProperty.call(actions, action.id)) throw new Error(`duplicate action id ${action.id}`);
|
|
@@ -287,8 +394,7 @@ function replayCase(caseDef, distribution, options = {}) {
|
|
|
287
394
|
return {
|
|
288
395
|
actions,
|
|
289
396
|
state: snapshot(handle),
|
|
290
|
-
|
|
291
|
-
pilotReportOrigin: 'harness'
|
|
397
|
+
...runtimePilotOf(handle)
|
|
292
398
|
};
|
|
293
399
|
} finally {
|
|
294
400
|
disposeSession(handle);
|
|
@@ -29,6 +29,7 @@ const SHARED_FILES = [
|
|
|
29
29
|
['hooks/scripts/lib/workflow-obligations.js', 'plugins/harness-everything/hooks/scripts/lib/workflow-obligations.js'],
|
|
30
30
|
['hooks/scripts/workflow-disposition.js', 'plugins/harness-everything/hooks/scripts/workflow-disposition.js'],
|
|
31
31
|
['hooks/scripts/workflow-stop-gate.js', 'plugins/harness-everything/hooks/scripts/workflow-stop-gate.js'],
|
|
32
|
+
['hooks/scripts/lib/doc-reconciliation-pilot.js', 'plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-pilot.js'],
|
|
32
33
|
];
|
|
33
34
|
|
|
34
35
|
let failed = 0;
|
|
@@ -91,7 +92,7 @@ checkCase('expected-RED report: legs agree, controls are green, no pilot credit,
|
|
|
91
92
|
assert.strictEqual(item.parity, 'match', `${item.caseId} source and packaged legs agree`);
|
|
92
93
|
}
|
|
93
94
|
const controls = report.cases.filter(item => item.status === 'CONTROL_PASS').map(item => item.caseId).sort();
|
|
94
|
-
assert.deepStrictEqual(controls, ['P02d', 'P06', 'P07u']);
|
|
95
|
+
assert.deepStrictEqual(controls, ['P01', 'P02a', 'P02b', 'P02c', 'P02d', 'P02f', 'P02g', 'P04', 'P06', 'P07-fable-multi-agent-workspace', 'P07-fable-parallel', 'P07-fable-staged', 'P07u']);
|
|
95
96
|
});
|
|
96
97
|
|
|
97
98
|
checkCase('gap registry: every entry is qualified and the expected-RED mode accounts for all of them', () => {
|
|
@@ -102,8 +103,8 @@ checkCase('gap registry: every entry is qualified and the expected-RED mode acco
|
|
|
102
103
|
});
|
|
103
104
|
|
|
104
105
|
checkCase('strict mode fails on the recorded gaps and preserves the RED log', () => {
|
|
105
|
-
const log = path.join(workDir, '
|
|
106
|
-
const run = runRunner(['--strict', '--case', '
|
|
106
|
+
const log = path.join(workDir, 'p02e-red.json');
|
|
107
|
+
const run = runRunner(['--strict', '--case', 'P02e', '--log', log]);
|
|
107
108
|
assert.strictEqual(run.status, 1, `strict mode exits 1 on gaps: ${run.stdout}`);
|
|
108
109
|
const saved = JSON.parse(fs.readFileSync(log, 'utf8'));
|
|
109
110
|
assert.ok(saved.cases[0].assertions.some(entry => entry.status === 'gap-qualified'), 'the log keeps the qualified gaps');
|
|
@@ -116,8 +117,8 @@ checkCase('malformed registry is an infrastructure error, not a RED pass', () =>
|
|
|
116
117
|
});
|
|
117
118
|
|
|
118
119
|
checkCase('a registry that omits a gap fails expected-RED mode', () => {
|
|
119
|
-
const file = writeRegistry('omit-gap.json', copy => { copy.entries = copy.entries.filter(entry => entry.assertionId !== '
|
|
120
|
-
const run = runRunner(['--expect-red', '--case', '
|
|
120
|
+
const file = writeRegistry('omit-gap.json', copy => { copy.entries = copy.entries.filter(entry => entry.assertionId !== 'P02e.on.review-pass-exit' || entry.distribution !== 'source'); });
|
|
121
|
+
const run = runRunner(['--expect-red', '--case', 'P02e', '--registry', file]);
|
|
121
122
|
assert.strictEqual(run.status, 1, `an unqualified gap fails: ${run.stdout}`);
|
|
122
123
|
});
|
|
123
124
|
|
|
@@ -152,9 +153,8 @@ checkCase('negative control: an abnormal route in P01 is infrastructure, and --e
|
|
|
152
153
|
assert.deepStrictEqual(after.map(item => [item.id, item.pass, item.actual]), before.map(item => [item.id, item.pass, item.actual]),
|
|
153
154
|
`${leg}: the later assertions evaluate exactly as in the unmodified replay`);
|
|
154
155
|
for (const item of after) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
`${leg}: ${item.id} still matches its registry actual, so only the infrastructure rule can stop qualification`);
|
|
156
|
+
assert.strictEqual(item.pass, true,
|
|
157
|
+
`${leg}: ${item.id} remains strict S1 GREEN, so only the infrastructure rule can stop the replay`);
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
const result = runner.classifyCase(p01, injected, p01Entries);
|