harness-everything 0.36.3 → 0.36.4

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.
Files changed (47) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +10 -0
  4. package/ci/fixtures/doc-obligations/read-boundary/boundary-controls.json +211 -0
  5. package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +325 -0
  6. package/ci/fixtures/doc-obligations/read-boundary/cases.json +14058 -0
  7. package/ci/fixtures/doc-obligations/read-boundary/expected.json +1 -0
  8. package/ci/fixtures/doc-obligations/read-boundary/identity-cases.json +119 -0
  9. package/ci/fixtures/doc-obligations/read-boundary/identity-expected.json +1 -0
  10. package/ci/fixtures/doc-obligations/read-boundary/scope-cases.json +208 -0
  11. package/ci/fixtures/doc-obligations/read-boundary/scope-expected.json +1 -0
  12. package/ci/fixtures/doc-obligations/reconciliation-prep/cases.json +2751 -0
  13. package/ci/fixtures/doc-obligations/reconciliation-prep/flood-entrypoint.js +5 -0
  14. package/ci/fixtures/doc-obligations/reconciliation-prep/hang-entrypoint.js +5 -0
  15. package/ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json +648 -0
  16. package/ci/fixtures/doc-obligations/reconciliation-prep/provider-contract.json +100 -0
  17. package/ci/fixtures/doc-obligations/reconciliation-prep/trust-contract.json +188 -0
  18. package/ci/lib/doc-evidence-boundary-check.js +1240 -0
  19. package/ci/lib/doc-evidence-fixtures.js +459 -0
  20. package/ci/lib/doc-evidence-trace.js +171 -0
  21. package/ci/lib/doc-reconciliation-prep-fixtures.js +502 -0
  22. package/ci/lib/snapshot-stdin.js +27 -0
  23. package/ci/mechanism-325-3b-preparation.test.js +382 -0
  24. package/ci/mechanism-341-boundary-enforcement.test.js +660 -0
  25. package/ci/mechanism-341-capture-module.test.js +357 -0
  26. package/ci/mechanism-341-mutation-runner.test.js +413 -0
  27. package/ci/mechanism-341-read-boundary.test.js +388 -0
  28. package/ci/mechanism-341-snapshot-stdin.test.js +40 -0
  29. package/ci/mutation-check.js +805 -45
  30. package/ci/mutation-loader.js +544 -57
  31. package/ci/prep/doc-reconciliation-red.js +407 -0
  32. package/ci/stress-341-boundary-control.js +85 -0
  33. package/docs/doc-evidence-baseline-binding.md +105 -19
  34. package/docs/doc-evidence-boundary.md +135 -0
  35. package/docs/doc-workflow-reconciliation.md +224 -0
  36. package/docs/document-lifecycle.md +8 -1
  37. package/harness-everything/SKILL.md +1 -1
  38. package/harness-everything/scripts/doc-evidence-capture.js +331 -0
  39. package/harness-everything/scripts/doc-evidence.js +72 -223
  40. package/opencode-plugin/plugin.json +1 -1
  41. package/package.json +5 -3
  42. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  43. package/plugins/harness-everything/plugin.json +1 -1
  44. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  45. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence-capture.js +331 -0
  46. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +72 -223
  47. package/sbom.cdx.json +43 -7
@@ -0,0 +1,502 @@
1
+ 'use strict';
2
+
3
+ // #325 Phase 3b preparation: isolated replay helper for the document-reconciliation RED corpus.
4
+ //
5
+ // Each replay creates a fresh workspace and state home, runs ordered actions through one explicitly
6
+ // selected distribution, and returns a replay root with the L1 shape:
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 only as a request; no reader for
9
+ // it exists in this preparation slice, so ON replays run the legacy code path. Fixture operations
10
+ // (provideEvidence, mutateIdentity, timestampHints) write simulated records only inside the replay's own
11
+ // temporary directories. They are contract fixtures, not production commands, and nothing reads them yet.
12
+
13
+ const crypto = require('crypto');
14
+ const fs = require('fs');
15
+ const os = require('os');
16
+ const path = require('path');
17
+ const { spawnSync } = require('child_process');
18
+
19
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
20
+ const PILOT_SWITCH = 'HARNESS_DOC_RECONCILIATION_PILOT';
21
+ const FIXTURE_OPS = new Set(['provideEvidence', 'mutateIdentity', 'timestampHints']);
22
+ const CONTROLLER_OPS = new Set(['plan', 'start', 'binding', 'step', 'obligation']);
23
+ const ALL_OPS = new Set(['route', 'stop', ...CONTROLLER_OPS, ...FIXTURE_OPS]);
24
+
25
+ // Explicit distributions. Every entry point is named by absolute path; no module is reused across legs.
26
+ const DISTRIBUTIONS = Object.freeze({
27
+ source: {
28
+ root: REPO_ROOT,
29
+ controller: path.join(REPO_ROOT, 'hooks', 'scripts', 'workflow-disposition.js'),
30
+ stopGate: path.join(REPO_ROOT, 'hooks', 'scripts', 'workflow-stop-gate.js'),
31
+ kernel: path.join(REPO_ROOT, 'harness-everything', 'scripts', 'kernel-router.js')
32
+ },
33
+ packaged: {
34
+ root: path.join(REPO_ROOT, 'plugins', 'harness-everything'),
35
+ controller: path.join(REPO_ROOT, 'plugins', 'harness-everything', 'hooks', 'scripts', 'workflow-disposition.js'),
36
+ 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')
38
+ }
39
+ });
40
+
41
+ // Child-process limits for every replay subprocess: a deadline, an output bound and an uncatchable kill. The deadline
42
+ // is about 150x the slowest real action measured on the corpus (65 ms over 440 spawns), so it keeps headroom on slow
43
+ // CI hosts. A test copy of the corpus may shorten it per case and substitute one fixture script for one operation
44
+ // (see limitsFor). The committed corpus carries neither, and the production CLI never reads an environment switch.
45
+ const CHILD_LIMITS = Object.freeze({ timeoutMs: 10000, maxBuffer: 4 * 1024 * 1024, killSignal: 'SIGKILL' });
46
+ const FIXTURE_DIR = path.join(__dirname, '..', 'fixtures', 'doc-obligations', 'reconciliation-prep');
47
+ const SUBSTITUTABLE_OPS = new Set(['route', 'stop', ...CONTROLLER_OPS]);
48
+ const LIMIT_KEYS = new Set(['timeoutMs', 'substitutes']);
49
+
50
+ function limitsFor(caseDef) {
51
+ const override = caseDef.childLimits;
52
+ if (override === undefined) return { ...CHILD_LIMITS, substitutes: {} };
53
+ if (!override || typeof override !== 'object' || Array.isArray(override)) throw new Error(`${caseDef.id} childLimits must be an object`);
54
+ for (const key of Object.keys(override)) {
55
+ if (!LIMIT_KEYS.has(key)) throw new Error(`${caseDef.id} childLimits has unknown key ${key}`);
56
+ }
57
+ const timeoutMs = override.timeoutMs === undefined ? CHILD_LIMITS.timeoutMs : override.timeoutMs;
58
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > CHILD_LIMITS.timeoutMs) {
59
+ throw new Error(`${caseDef.id} childLimits.timeoutMs may only shorten the ${CHILD_LIMITS.timeoutMs} ms deadline`);
60
+ }
61
+ const substitutes = override.substitutes === undefined ? {} : override.substitutes;
62
+ for (const [op, file] of Object.entries(substitutes)) {
63
+ if (!SUBSTITUTABLE_OPS.has(op)) throw new Error(`${caseDef.id} childLimits.substitutes names unsupported op ${op}`);
64
+ if (typeof file !== 'string' || !/^[a-z0-9-]+\.js$/.test(file) || !fs.existsSync(path.join(FIXTURE_DIR, file))) {
65
+ throw new Error(`${caseDef.id} childLimits.substitutes.${op} must name a script in the prep fixture directory`);
66
+ }
67
+ }
68
+ return { ...CHILD_LIMITS, timeoutMs, substitutes: { ...substitutes } };
69
+ }
70
+
71
+ // Environment for every child. Caller-provided HARNESS_* state is removed, and only the pilot switch
72
+ // explicitly requested is set. Any other inherited switch cannot change the replay.
73
+ function childEnv(workspace, stateHome, pilot) {
74
+ const env = { ...process.env };
75
+ for (const key of Object.keys(env)) {
76
+ if (key.startsWith('HARNESS_')) delete env[key];
77
+ }
78
+ delete env.CLAUDE_PLUGIN_ROOT;
79
+ delete env.PLUGIN_ROOT;
80
+ env.HARNESS_STATE_HOME = stateHome;
81
+ env.HARNESS_WORKSPACE_ROOT = workspace;
82
+ if (pilot !== undefined) env[PILOT_SWITCH] = pilot;
83
+ return env;
84
+ }
85
+
86
+ function createSession(distribution, caseId, pilot) {
87
+ if (!DISTRIBUTIONS[distribution]) throw new Error(`unknown distribution ${distribution}`);
88
+ const token = crypto.randomBytes(4).toString('hex');
89
+ const workspace = fs.mkdtempSync(path.join(os.tmpdir(), `doc-recon-${caseId}-${distribution}-ws-${token}-`));
90
+ const stateHome = fs.mkdtempSync(path.join(os.tmpdir(), `doc-recon-${caseId}-${distribution}-state-${token}-`));
91
+ return {
92
+ distribution,
93
+ caseId,
94
+ pilot,
95
+ workspace,
96
+ stateHome,
97
+ sessionId: `${caseId.toLowerCase()}-session`
98
+ };
99
+ }
100
+
101
+ function disposeSession(handle) {
102
+ fs.rmSync(handle.workspace, { recursive: true, force: true });
103
+ fs.rmSync(handle.stateHome, { recursive: true, force: true });
104
+ }
105
+
106
+ function runScript(handle, script, args, stdin, pilot) {
107
+ return spawnSync(process.execPath, [script, ...args], {
108
+ cwd: handle.workspace,
109
+ encoding: 'utf8',
110
+ input: stdin === undefined ? undefined : JSON.stringify(stdin),
111
+ env: childEnv(handle.workspace, handle.stateHome, pilot),
112
+ timeout: handle.limits.timeoutMs,
113
+ maxBuffer: handle.limits.maxBuffer,
114
+ killSignal: handle.limits.killSignal
115
+ });
116
+ }
117
+
118
+ // A test copy of the corpus may substitute one fixture script for an operation; otherwise the distribution's entry runs.
119
+ function scriptFor(handle, op, fallback) {
120
+ const name = handle.limits.substitutes[op];
121
+ return name === undefined ? fallback : path.join(FIXTURE_DIR, name);
122
+ }
123
+
124
+ // The session directory is the only workflow-run.json under the fresh state home (one session per replay).
125
+ function sessionDirOf(handle) {
126
+ const found = [];
127
+ const walk = dir => {
128
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
129
+ const full = path.join(dir, entry.name);
130
+ if (entry.isDirectory()) walk(full);
131
+ else if (entry.name === 'workflow-run.json') found.push(dir);
132
+ }
133
+ };
134
+ if (fs.existsSync(handle.stateHome)) walk(handle.stateHome);
135
+ if (found.length === 0) return null;
136
+ if (found.length > 1) throw new Error(`expected one session directory under ${handle.stateHome}, found ${found.length}`);
137
+ return found[0];
138
+ }
139
+
140
+ function readJsonOrNull(file) {
141
+ try {
142
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
143
+ } catch (_) {
144
+ return null;
145
+ }
146
+ }
147
+
148
+ function snapshot(handle) {
149
+ const dir = sessionDirOf(handle);
150
+ if (!dir) return { workflow: null, obligations: null, handoff: null };
151
+ return {
152
+ workflow: readJsonOrNull(path.join(dir, 'workflow-run.json')),
153
+ obligations: readJsonOrNull(path.join(dir, 'workflow-obligations.json')),
154
+ handoff: readJsonOrNull(path.join(dir, 'handoff-state.json'))
155
+ };
156
+ }
157
+
158
+ function parseJsonOrNull(text) {
159
+ try {
160
+ return JSON.parse(text);
161
+ } catch (_) {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ function fixtureResult(handle, pilot, payloadSummary) {
167
+ return {
168
+ exitCode: 0,
169
+ signal: null,
170
+ error: null,
171
+ errorCode: null,
172
+ stdout: JSON.stringify({ fixture: true, ...payloadSummary }),
173
+ stderr: '',
174
+ json: { fixture: true, ...payloadSummary },
175
+ state: snapshot(handle),
176
+ pilot
177
+ };
178
+ }
179
+
180
+ // Fixture operations write simulated records inside the replay's own temporary directories only.
181
+ function runFixture(handle, action, pilot) {
182
+ const p = action.payload || {};
183
+ if (action.op === 'provideEvidence') {
184
+ if (typeof p.id !== 'string' || !/^[a-z0-9-]+$/.test(p.id)) throw new Error('provideEvidence needs a stable id');
185
+ const dir = path.join(handle.stateHome, 'prep-host-events');
186
+ fs.mkdirSync(dir, { recursive: true });
187
+ fs.writeFileSync(path.join(dir, `${p.id}.json`), JSON.stringify(p.record || {}, null, 2), 'utf8');
188
+ return fixtureResult(handle, pilot, { op: 'provideEvidence', id: p.id });
189
+ }
190
+ if (action.op === 'mutateIdentity') {
191
+ if (typeof p.path !== 'string' || path.isAbsolute(p.path) || p.path.split(/[\\/]/).includes('..')) {
192
+ throw new Error('mutateIdentity path must be workspace-relative');
193
+ }
194
+ const target = path.join(handle.workspace, p.path);
195
+ fs.mkdirSync(path.dirname(target), { recursive: true });
196
+ fs.writeFileSync(target, String(p.content), 'utf8');
197
+ return fixtureResult(handle, pilot, { op: 'mutateIdentity', kind: p.kind, path: p.path });
198
+ }
199
+ if (action.op === 'timestampHints') {
200
+ const dir = sessionDirOf(handle);
201
+ if (!dir) throw new Error('timestampHints needs an existing session');
202
+ const file = path.join(dir, 'handoff-state.json');
203
+ const current = readJsonOrNull(file) || {};
204
+ const next = { ...current };
205
+ if (p.lastEditAt === 'early') next.lastEditAt = 1000;
206
+ if (p.lastVerifyAt === 'now') next.lastVerifyAt = Date.now();
207
+ fs.writeFileSync(file, JSON.stringify(next, null, 2), 'utf8');
208
+ return fixtureResult(handle, pilot, { op: 'timestampHints' });
209
+ }
210
+ throw new Error(`unknown fixture op ${action.op}`);
211
+ }
212
+
213
+ // Actions are the only way replay state changes. Each controller/stop op maps to the existing public entry point.
214
+ function runAction(handle, action, pilot) {
215
+ const dist = DISTRIBUTIONS[handle.distribution];
216
+ const sid = handle.sessionId;
217
+ const p = action.payload || {};
218
+ if (!ALL_OPS.has(action.op)) throw new Error(`unsupported replay op ${action.op}`);
219
+ if (FIXTURE_OPS.has(action.op)) return runFixture(handle, action, pilot);
220
+ let result;
221
+ if (action.op === 'route') {
222
+ result = runScript(handle, scriptFor(handle, 'route', dist.kernel), [], { session_id: sid, cwd: handle.workspace, prompt: p.prompt }, pilot);
223
+ } else if (action.op === 'stop') {
224
+ result = runScript(handle, scriptFor(handle, 'stop', dist.stopGate), [], { session_id: sid, cwd: handle.workspace, hook_event_name: 'Stop' }, pilot);
225
+ } else if (action.op === 'plan') {
226
+ result = runScript(handle, scriptFor(handle, 'plan', dist.controller), [
227
+ 'plan', '--session-id', sid, '--requirements-json', JSON.stringify(p.requirements),
228
+ '--strategy', p.strategy, '--evidence', p.evidence
229
+ ], undefined, pilot);
230
+ } else if (action.op === 'start') {
231
+ result = runScript(handle, scriptFor(handle, 'start', dist.controller), ['start', '--session-id', sid], undefined, pilot);
232
+ } else if (action.op === 'binding') {
233
+ result = runScript(handle, scriptFor(handle, 'binding', dist.controller), [
234
+ 'binding', '--session-id', sid, '--step-id', p.stepId, '--binding-id', p.bindingId,
235
+ '--disposition', p.disposition, '--evidence', p.evidence
236
+ ], undefined, pilot);
237
+ } else if (action.op === 'step') {
238
+ result = runScript(handle, scriptFor(handle, 'step', dist.controller), [
239
+ 'step', '--session-id', sid, '--step-id', p.stepId, '--disposition', p.disposition, '--evidence', p.evidence
240
+ ], undefined, pilot);
241
+ } else if (action.op === 'obligation') {
242
+ const args = [
243
+ 'obligation', '--session-id', sid, '--obligation-id', p.obligationId,
244
+ '--disposition', p.disposition, '--evidence', p.evidence
245
+ ];
246
+ if (p.reasonCode) args.push('--reason-code', p.reasonCode);
247
+ if (p.scope) args.push('--scope', p.scope);
248
+ result = runScript(handle, scriptFor(handle, 'obligation', dist.controller), args, undefined, pilot);
249
+ }
250
+ return {
251
+ exitCode: result.status,
252
+ signal: result.signal,
253
+ error: result.error ? result.error.message : null,
254
+ errorCode: result.error ? result.error.code || null : null,
255
+ stdout: result.stdout || '',
256
+ stderr: result.stderr || '',
257
+ json: parseJsonOrNull(result.stdout || ''),
258
+ state: snapshot(handle),
259
+ pilot
260
+ };
261
+ }
262
+
263
+ // The effective switch for one action. An OFF replay (no replay-level request) stays OFF for every action, so a
264
+ // per-action override can only exist inside an ON replay. Inside an ON replay an explicit per-action value wins.
265
+ function effectivePilot(action, replayPilot) {
266
+ if (replayPilot === undefined) return undefined;
267
+ return Object.prototype.hasOwnProperty.call(action, 'pilot') ? action.pilot : replayPilot;
268
+ }
269
+
270
+ // Replays one case through one distribution. Actions run in order and the replay stops at its first infrastructure
271
+ // fault: the faulted action is recorded, no later action runs, and the temporary directories are still removed.
272
+ // A crash is recorded, not hidden.
273
+ function replayCase(caseDef, distribution, options = {}) {
274
+ const limits = limitsFor(caseDef);
275
+ const mode = options.pilot === undefined ? 'off' : 'on';
276
+ const handle = { ...createSession(distribution, caseDef.id, options.pilot), distribution, limits };
277
+ try {
278
+ const actions = {};
279
+ for (const action of caseDef.actions) {
280
+ if (Object.prototype.hasOwnProperty.call(actions, action.id)) throw new Error(`duplicate action id ${action.id}`);
281
+ const payload = action.payload && action.payload.requirements === '$requirements'
282
+ ? { ...action.payload, requirements: caseDef.requirements }
283
+ : action.payload;
284
+ actions[action.id] = runAction(handle, { ...action, payload }, effectivePilot(action, options.pilot));
285
+ if (actionFaults(action, actions[action.id], mode).length) break;
286
+ }
287
+ return {
288
+ actions,
289
+ state: snapshot(handle),
290
+ pilot: null,
291
+ pilotReportOrigin: 'harness'
292
+ };
293
+ } finally {
294
+ disposeSession(handle);
295
+ }
296
+ }
297
+
298
+ // Per-action expected-exit policy. `expectExit` is optional and per mode: { off, on }, each an integer or a
299
+ // non-empty array of integers. A mode it omits permits exit 0 only. The policy names what the process may do
300
+ // without being an infrastructure failure. It does not name the desired outcome: an ON assertion that wants exit 2
301
+ // while the process exits 0 stays a qualified gap, not a policy violation.
302
+ const DEFAULT_EXIT = Object.freeze([0]);
303
+ const EXIT_POLICY_MODES = new Set(['off', 'on']);
304
+
305
+ function exitPolicy(action, mode) {
306
+ if (action.expectExit === undefined) return DEFAULT_EXIT;
307
+ const policy = action.expectExit;
308
+ if (!policy || typeof policy !== 'object' || Array.isArray(policy)) throw new Error(`${action.id} expectExit must be {off, on}`);
309
+ for (const key of Object.keys(policy)) {
310
+ if (!EXIT_POLICY_MODES.has(key)) throw new Error(`${action.id} expectExit has unknown mode ${key}`);
311
+ }
312
+ if (policy[mode] === undefined) return DEFAULT_EXIT;
313
+ const codes = Array.isArray(policy[mode]) ? policy[mode] : [policy[mode]];
314
+ if (codes.length === 0 || !codes.every(code => Number.isInteger(code) && code >= 0 && code <= 255)) {
315
+ throw new Error(`${action.id} expectExit.${mode} must be an exit code or a non-empty array of exit codes`);
316
+ }
317
+ return codes;
318
+ }
319
+
320
+ // Required output of an accepted (exit 0) action. Routing must print its checkpoint, and controller ops must print
321
+ // the JSON envelope with a string state. Stop and fixture ops have no required output here.
322
+ const ROUTE_CHECKPOINT = /^\[Tier Routing Pre-check\][\s\S]*RECOMMENDED TIER:/;
323
+
324
+ function missingRequiredOutput(action, result) {
325
+ if (action.op === 'route') return !ROUTE_CHECKPOINT.test(result.stdout);
326
+ if (CONTROLLER_OPS.has(action.op)) {
327
+ return !(result.json !== null && typeof result.json === 'object' && !Array.isArray(result.json) && typeof result.json.state === 'string');
328
+ }
329
+ return false;
330
+ }
331
+
332
+ // The one fault predicate. Fault messages for one action's result: a child deadline or output bound hit, a spawn
333
+ // error, a signal (the forced kill included), no exit status, an exit outside the action's policy, or missing required
334
+ // output on an accepted exit. replayCase stops on it, and infrastructureFaults classifies with it.
335
+ function actionFaults(action, result, mode) {
336
+ const faults = [];
337
+ if (result.errorCode === 'ETIMEDOUT') faults.push('timed out at the child deadline (ETIMEDOUT)');
338
+ else if (result.errorCode === 'ENOBUFS') faults.push('child output exceeded maxBuffer (ENOBUFS)');
339
+ else if (result.error) faults.push(`spawn error ${result.error}`);
340
+ if (result.signal) faults.push(`terminated by signal ${result.signal}`);
341
+ if (result.exitCode === null) return [...faults, 'no exit status'];
342
+ const policy = exitPolicy(action, mode);
343
+ if (!policy.includes(result.exitCode)) return [...faults, `exit ${result.exitCode} outside policy [${policy.join(', ')}]`];
344
+ if (result.exitCode === 0 && missingRequiredOutput(action, result)) faults.push('malformed required output');
345
+ return faults;
346
+ }
347
+
348
+ // Infrastructure faults of one replay, as { actionId, message }. Only actions that ran are judged, so a replay that
349
+ // stopped at its first fault is classified by the same predicate that stopped it. Any fault makes the case an
350
+ // infrastructure failure, decided before registry matching or parity, so a faulted replay never qualifies as an
351
+ // expected RED gap.
352
+ function infrastructureFaults(caseDef, root, mode) {
353
+ return caseDef.actions
354
+ .filter(action => Object.prototype.hasOwnProperty.call(root.actions, action.id))
355
+ .flatMap(action => actionFaults(action, root.actions[action.id], mode).map(message => ({ actionId: action.id, message })));
356
+ }
357
+
358
+ const VOLATILE_KEYS = new Set([
359
+ 'updatedAt', 'recordedAt', 'resolvedAt', 'createdAt', 'lastMutationAt', 'lastEditAt', 'lastVerifyAt',
360
+ 'workflowId', 'runId', 'workspaceId', 'startedAt', 'completedAt', 'revision'
361
+ ]);
362
+ const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/;
363
+ const UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g;
364
+
365
+ // Presence-preserving normalizer for comparing two legs: volatile values become markers, keys stay.
366
+ function normalize(value) {
367
+ if (Array.isArray(value)) return value.map(normalize);
368
+ if (value && typeof value === 'object') {
369
+ const out = {};
370
+ for (const [key, item] of Object.entries(value)) {
371
+ const volatile = (VOLATILE_KEYS.has(key) || (/Ms$/.test(key) && typeof item === 'number')) && item !== null;
372
+ out[key] = volatile ? '<volatile>' : normalize(item);
373
+ }
374
+ return out;
375
+ }
376
+ if (typeof value === 'string' && ISO.test(value)) return '<iso>';
377
+ if (typeof value === 'string') return scrubText(value);
378
+ return value;
379
+ }
380
+
381
+ function escapeRegExp(text) {
382
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
383
+ }
384
+
385
+ // Separators are folded before any marker is applied, so Windows-shaped and POSIX-shaped paths meet the same
386
+ // patterns. Folding happens on the text, not on the marker set, so no marker depends on the host separator.
387
+ function posixForm(value) {
388
+ return String(value).replace(/\\/g, '/');
389
+ }
390
+
391
+ // Replaces each maximal run of characters outside [\s'"] that contains `needle`. This matches what
392
+ // /[^\s'"]*needle[^\s'"]*/g replaces, but in linear time: that regex backtracks quadratically on one long token,
393
+ // and a flooded child stdout is exactly such a token (a 4 MB run took minutes; the scan takes milliseconds).
394
+ function replaceTokensContaining(text, needle, replacement) {
395
+ return text.split(/([\s'"]+)/).map((part, index) => (index % 2 === 1 || !part.includes(needle) ? part : replacement)).join('');
396
+ }
397
+
398
+ let defaultRoots = null;
399
+ // Roots of this checkout and of the temporary directory, both as the host reports them and as realpath resolves
400
+ // them (macOS /var is a symlink to /private/var; Windows short and long names can differ).
401
+ function scrubRoots() {
402
+ if (defaultRoots === null) {
403
+ const tmp = [os.tmpdir()];
404
+ try {
405
+ tmp.push(fs.realpathSync(os.tmpdir()));
406
+ } catch (_) {
407
+ // The unresolved directory above still scrubs its own prefix.
408
+ }
409
+ defaultRoots = { repo: REPO_ROOT, tmp };
410
+ }
411
+ return defaultRoots;
412
+ }
413
+
414
+ // Case-insensitive, because Windows volumes and tmp names are case-preserving but case-insensitive.
415
+ function literalPath(value) {
416
+ return new RegExp(escapeRegExp(posixForm(value)), 'gi');
417
+ }
418
+
419
+ // Replaces this replay's temporary paths, the checkout root and skill locations in raw text.
420
+ function scrubText(text, roots = scrubRoots()) {
421
+ let out = replaceTokensContaining(posixForm(text), 'doc-recon-', '<tmp>')
422
+ .replace(/\/(private\/)?var\/folders\/[^\s'"]+/g, '<tmp>')
423
+ .replace(/\/tmp\/[^\s'"]+/g, '<tmp>');
424
+ for (const dir of roots.tmp) out = out.replace(literalPath(dir), '<tmp>');
425
+ out = out.replace(UUID, '<uuid>')
426
+ .replace(literalPath(`${roots.repo}/plugins/harness-everything/`), '<dist>/')
427
+ .replace(literalPath(`${roots.repo}/`), '<dist>/');
428
+ return out.replace(/<dist>\/(?:skills\/)?([a-z0-9-]+)\/SKILL\.md/g, '<skill:$1>');
429
+ }
430
+
431
+ function scrubReplay(root) {
432
+ const out = JSON.parse(JSON.stringify(root));
433
+ for (const action of Object.values(out.actions)) {
434
+ action.stdout = scrubText(action.stdout);
435
+ action.stderr = scrubText(action.stderr);
436
+ }
437
+ return out;
438
+ }
439
+
440
+ const ABSENT = Symbol('absent');
441
+
442
+ // Walks a path of keys and indices. Own-key absence is reported distinctly from a null value.
443
+ function lookup(root, pathKeys) {
444
+ let node = root;
445
+ for (const key of pathKeys) {
446
+ if (node === null || typeof node !== 'object' || !Object.prototype.hasOwnProperty.call(node, key)) return ABSENT;
447
+ node = node[key];
448
+ }
449
+ return node;
450
+ }
451
+
452
+ const sameJson = (left, right) => JSON.stringify(normalize(left)) === JSON.stringify(normalize(right));
453
+
454
+ // Operators: equals / notEquals (deep JSON equality), includes (array member or substring), absent (own key missing).
455
+ function evaluateAssertion(root, assertion) {
456
+ const actual = lookup(root, assertion.path);
457
+ const actualJson = actual === ABSENT ? { kind: 'absent' } : actual;
458
+ let pass;
459
+ if (assertion.operator === 'equals') {
460
+ pass = actual !== ABSENT && sameJson(actual, assertion.expected);
461
+ } else if (assertion.operator === 'notEquals') {
462
+ pass = actual !== ABSENT && !sameJson(actual, assertion.expected);
463
+ } else if (assertion.operator === 'includes') {
464
+ pass = actual !== ABSENT && (
465
+ (Array.isArray(actual) && actual.some(item => sameJson(item, assertion.expected)))
466
+ || (typeof actual === 'string' && typeof assertion.expected === 'string' && actual.includes(assertion.expected))
467
+ );
468
+ } else if (assertion.operator === 'absent') {
469
+ pass = actual === ABSENT;
470
+ } else {
471
+ throw new Error(`unknown operator ${assertion.operator}`);
472
+ }
473
+ return { id: assertion.id, pass, actual: actualJson, expected: assertion.operator === 'absent' ? null : assertion.expected };
474
+ }
475
+
476
+ function evaluateAll(root, assertions) {
477
+ return assertions.map(assertion => evaluateAssertion(root, assertion));
478
+ }
479
+
480
+ function readCaseCorpus(file) {
481
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
482
+ }
483
+
484
+ module.exports = {
485
+ DISTRIBUTIONS,
486
+ CHILD_LIMITS,
487
+ FIXTURE_DIR,
488
+ PILOT_SWITCH,
489
+ ALL_OPS,
490
+ replayCase,
491
+ exitPolicy,
492
+ actionFaults,
493
+ infrastructureFaults,
494
+ scrubReplay,
495
+ scrubText,
496
+ normalize,
497
+ evaluateAll,
498
+ evaluateAssertion,
499
+ lookup,
500
+ readCaseCorpus,
501
+ ABSENT
502
+ };
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+
8
+ // Give a synchronous checker a finite stdin snapshot instead of coordinating
9
+ // pipe EOF with spawnSync. The private file and descriptor belong to this call.
10
+ function spawnWithSnapshotInput(executable, args, options = {}) {
11
+ if (options.input === undefined) return spawnSync(executable, args, options);
12
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-checker-input-'));
13
+ let fd;
14
+ try {
15
+ const file = path.join(temp, 'input.json');
16
+ fs.writeFileSync(file, options.input, { mode: 0o600 });
17
+ fd = fs.openSync(file, 'r');
18
+ const invocation = { ...options, stdio: [fd, 'pipe', 'pipe'] };
19
+ delete invocation.input;
20
+ return spawnSync(executable, args, invocation);
21
+ } finally {
22
+ if (fd !== undefined) fs.closeSync(fd);
23
+ fs.rmSync(temp, { recursive: true, force: true });
24
+ }
25
+ }
26
+
27
+ module.exports = { spawnWithSnapshotInput };