harness-everything 0.13.0 → 0.15.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 +33 -0
- package/ci/contract-integrity.test.js +155 -1
- package/ci/mechanism-2ak-contract-probe-adapter.test.js +207 -0
- package/ci/openai-plugin-package.test.js +10 -0
- package/contract-integrity/ADAPTERS.md +37 -0
- package/contract-integrity/fixtures/node-probe-project/package.json +8 -0
- package/contract-integrity/fixtures/node-probe-project/probe-plan.json +27 -0
- package/contract-integrity/fixtures/node-probe-project/src/limit.js +8 -0
- package/contract-integrity/fixtures/node-probe-project/test/contract.test.js +14 -0
- package/contract-integrity/scripts/audit.js +225 -2
- package/contract-integrity/scripts/node-probe-adapter.js +390 -0
- 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/contract-integrity/ADAPTERS.md +37 -0
- package/plugins/harness-everything/contract-integrity/scripts/audit.js +225 -2
- package/plugins/harness-everything/contract-integrity/scripts/node-probe-adapter.js +390 -0
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/verification-loop/SKILL.md +2 -2
- package/plugins/harness-everything/skills/verification-loop/references/verification-phases.md +9 -1
- package/scripts/sync-openai-plugin.js +4 -1
- package/verification-loop/SKILL.md +2 -2
- package/verification-loop/references/verification-phases.md +9 -1
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const VERSION = '1.0.0';
|
|
11
|
+
const ADAPTER = 'node-npm-v1';
|
|
12
|
+
const STRATEGIES = new Set(['env', 'replace']);
|
|
13
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
14
|
+
const MAX_TIMEOUT_MS = 120000;
|
|
15
|
+
|
|
16
|
+
function isObject(value) {
|
|
17
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function safeRelative(value) {
|
|
21
|
+
if (typeof value !== 'string' || !value.trim() || path.isAbsolute(value)) return false;
|
|
22
|
+
const normalized = value.replace(/\\/g, '/');
|
|
23
|
+
return normalized !== '..' && !normalized.startsWith('../') && !normalized.includes('/../') && !normalized.includes('\0');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sha256(value) {
|
|
27
|
+
return crypto.createHash('sha256').update(String(value || ''), 'utf8').digest('hex');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validatePlan(plan) {
|
|
31
|
+
const errors = [];
|
|
32
|
+
if (!isObject(plan)) return ['probe plan must be an object'];
|
|
33
|
+
if (plan.schemaVersion !== VERSION) errors.push(`schemaVersion must be ${VERSION}`);
|
|
34
|
+
if (plan.adapter !== ADAPTER) errors.push(`adapter must be ${ADAPTER}`);
|
|
35
|
+
if (typeof plan.script !== 'string' || !/^[A-Za-z0-9:_-]+$/.test(plan.script)) errors.push('script must be an npm script name');
|
|
36
|
+
if (!Array.isArray(plan.probes) || plan.probes.length === 0) errors.push('probes must be a non-empty array');
|
|
37
|
+
const ids = new Set();
|
|
38
|
+
for (const [index, probe] of (Array.isArray(plan.probes) ? plan.probes : []).entries()) {
|
|
39
|
+
const p = `probes[${index}]`;
|
|
40
|
+
if (!isObject(probe)) { errors.push(`${p} must be an object`); continue; }
|
|
41
|
+
for (const field of ['probeId', 'requirementId', 'sourceSection', 'strategy', 'expectedFailureContains']) {
|
|
42
|
+
if (typeof probe[field] !== 'string' || !probe[field].trim()) errors.push(`${p}.${field} is required`);
|
|
43
|
+
}
|
|
44
|
+
if (probe.probeId && ids.has(probe.probeId)) errors.push(`${p}.probeId is duplicated: ${probe.probeId}`);
|
|
45
|
+
if (probe.probeId) ids.add(probe.probeId);
|
|
46
|
+
if (!STRATEGIES.has(probe.strategy)) errors.push(`${p}.strategy must be env or replace`);
|
|
47
|
+
if (probe.strategy === 'env') {
|
|
48
|
+
if (!isObject(probe.env) || Object.keys(probe.env).length === 0) errors.push(`${p}.env is required for env strategy`);
|
|
49
|
+
for (const [key, value] of Object.entries(isObject(probe.env) ? probe.env : {})) {
|
|
50
|
+
if (!/^[A-Z][A-Z0-9_]{0,79}$/.test(key)) errors.push(`${p}.env contains invalid key: ${key}`);
|
|
51
|
+
if (typeof value !== 'string' || value.length > 200) errors.push(`${p}.env.${key} must be a string <= 200 chars`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (probe.strategy === 'replace') {
|
|
55
|
+
if (!safeRelative(probe.target)) errors.push(`${p}.target must be a repo-relative path`);
|
|
56
|
+
if (typeof probe.find !== 'string' || probe.find.length === 0 || probe.find.length > 4000) errors.push(`${p}.find must be 1..4000 chars`);
|
|
57
|
+
if (typeof probe.replace !== 'string' || probe.replace.length > 4000) errors.push(`${p}.replace must be <= 4000 chars`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (plan.timeoutMs !== undefined && (!Number.isInteger(plan.timeoutMs) || plan.timeoutMs < 100 || plan.timeoutMs > MAX_TIMEOUT_MS)) {
|
|
61
|
+
errors.push(`timeoutMs must be an integer between 100 and ${MAX_TIMEOUT_MS}`);
|
|
62
|
+
}
|
|
63
|
+
return errors;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function discoverProject(workspace, plan) {
|
|
67
|
+
const packageFile = path.join(workspace, 'package.json');
|
|
68
|
+
if (!fs.existsSync(packageFile)) return { ok: false, reason: 'package-json-missing' };
|
|
69
|
+
let pkg;
|
|
70
|
+
try { pkg = JSON.parse(fs.readFileSync(packageFile, 'utf8')); }
|
|
71
|
+
catch (_) { return { ok: false, reason: 'package-json-invalid' }; }
|
|
72
|
+
if (!isObject(pkg.scripts) || typeof pkg.scripts[plan.script] !== 'string') {
|
|
73
|
+
return { ok: false, reason: 'npm-script-missing' };
|
|
74
|
+
}
|
|
75
|
+
const dependencyCount =
|
|
76
|
+
Object.keys(isObject(pkg.dependencies) ? pkg.dependencies : {}).length +
|
|
77
|
+
Object.keys(isObject(pkg.devDependencies) ? pkg.devDependencies : {}).length +
|
|
78
|
+
Object.keys(isObject(pkg.optionalDependencies) ? pkg.optionalDependencies : {}).length;
|
|
79
|
+
if (dependencyCount > 0) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
reason: 'external-dependencies-not-supported-v1',
|
|
83
|
+
detail: 'node-npm-v1 intentionally supports zero-external-dependency projects only; dependency installation is never inferred or networked',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
ok: true,
|
|
88
|
+
packageName: typeof pkg.name === 'string' ? pkg.name : null,
|
|
89
|
+
script: plan.script,
|
|
90
|
+
command: `npm run ${plan.script}`,
|
|
91
|
+
dependencyMode: 'zero-external-dependencies',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function copyWorkspace(source) {
|
|
96
|
+
const sourceRoot = path.resolve(source);
|
|
97
|
+
if (fs.lstatSync(sourceRoot).isSymbolicLink()) {
|
|
98
|
+
throw new Error('source workspace contains a symbolic link/reparse-point root');
|
|
99
|
+
}
|
|
100
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-contract-probe-'));
|
|
101
|
+
const destination = path.join(root, 'workspace');
|
|
102
|
+
try {
|
|
103
|
+
fs.cpSync(sourceRoot, destination, {
|
|
104
|
+
recursive: true,
|
|
105
|
+
filter: src => {
|
|
106
|
+
if (fs.lstatSync(src).isSymbolicLink()) {
|
|
107
|
+
throw new Error('source workspace contains a symbolic link/reparse-point component');
|
|
108
|
+
}
|
|
109
|
+
const relative = path.relative(sourceRoot, src).replace(/\\/g, '/');
|
|
110
|
+
if (!relative) return true;
|
|
111
|
+
const head = relative.split('/')[0];
|
|
112
|
+
return !['.git', 'node_modules', '.harness'].includes(head);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
} catch (error) {
|
|
116
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
return { root, workspace: destination };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function runScript(workspace, plan, extraEnv = {}) {
|
|
123
|
+
const timeout = plan.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
124
|
+
// On Windows npm is normally a .cmd shim, which child_process cannot
|
|
125
|
+
// execute directly without a command shell. The script name is restricted
|
|
126
|
+
// to a safe identifier by validatePlan(), and no user-provided shell text is
|
|
127
|
+
// interpolated into the command.
|
|
128
|
+
const result = spawnSync('npm', ['run', plan.script], {
|
|
129
|
+
cwd: workspace,
|
|
130
|
+
encoding: 'utf8',
|
|
131
|
+
timeout,
|
|
132
|
+
windowsHide: true,
|
|
133
|
+
shell: process.platform === 'win32',
|
|
134
|
+
env: { ...process.env, CI: '1', ...extraEnv },
|
|
135
|
+
});
|
|
136
|
+
const stdout = String(result.stdout || '');
|
|
137
|
+
const stderr = String(result.stderr || '');
|
|
138
|
+
const combined = stdout + '\n' + stderr;
|
|
139
|
+
if (result.error) {
|
|
140
|
+
const timedOut = result.error.code === 'ETIMEDOUT';
|
|
141
|
+
return {
|
|
142
|
+
kind: timedOut ? 'timeout' : 'spawn-error',
|
|
143
|
+
exitCode: null,
|
|
144
|
+
signal: result.signal || null,
|
|
145
|
+
outputHash: sha256(combined + String(result.error.message || '')),
|
|
146
|
+
combined,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
kind: 'exit',
|
|
151
|
+
exitCode: Number.isInteger(result.status) ? result.status : null,
|
|
152
|
+
signal: result.signal || null,
|
|
153
|
+
outputHash: sha256(combined),
|
|
154
|
+
combined,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isolatedRegularFile(workspace, relativeTarget) {
|
|
159
|
+
const root = path.resolve(workspace);
|
|
160
|
+
const target = path.resolve(root, relativeTarget);
|
|
161
|
+
const lexical = path.relative(root, target);
|
|
162
|
+
if (lexical === '..' || lexical.startsWith('..' + path.sep) || path.isAbsolute(lexical)) {
|
|
163
|
+
throw new Error('replace target escaped isolated workspace');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let current = root;
|
|
167
|
+
for (const segment of lexical.split(path.sep).filter(Boolean)) {
|
|
168
|
+
current = path.join(current, segment);
|
|
169
|
+
if (!fs.existsSync(current)) throw new Error('replace target does not exist');
|
|
170
|
+
const stat = fs.lstatSync(current);
|
|
171
|
+
if (stat.isSymbolicLink()) {
|
|
172
|
+
throw new Error('replace target contains symbolic link/reparse-point component');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const realRoot = fs.realpathSync(root);
|
|
177
|
+
const realTarget = fs.realpathSync(target);
|
|
178
|
+
const resolved = path.relative(realRoot, realTarget);
|
|
179
|
+
if (resolved === '..' || resolved.startsWith('..' + path.sep) || path.isAbsolute(resolved)) {
|
|
180
|
+
throw new Error('replace target resolved outside isolated workspace');
|
|
181
|
+
}
|
|
182
|
+
if (!fs.lstatSync(realTarget).isFile()) throw new Error('replace target does not exist');
|
|
183
|
+
return realTarget;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function applyProbe(workspace, probe) {
|
|
187
|
+
if (probe.strategy === 'env') return { env: { ...probe.env }, mutation: 'environment-only' };
|
|
188
|
+
|
|
189
|
+
const target = isolatedRegularFile(workspace, probe.target);
|
|
190
|
+
const source = fs.readFileSync(target, 'utf8');
|
|
191
|
+
const first = source.indexOf(probe.find);
|
|
192
|
+
const last = source.lastIndexOf(probe.find);
|
|
193
|
+
if (first < 0) throw new Error('replace find text was not present');
|
|
194
|
+
if (first !== last) throw new Error('replace find text must match exactly once');
|
|
195
|
+
fs.writeFileSync(target, source.slice(0, first) + probe.replace + source.slice(first + probe.find.length), 'utf8');
|
|
196
|
+
return { env: {}, mutation: `replace:${probe.target}` };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function evidenceRef(probe, run) {
|
|
200
|
+
return `probe:${probe.probeId}:output-sha256:${run.outputHash}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function notEvaluatedProbe(probe, reason, failureClass = 'infrastructure', workspaceIsolation = 'isolated') {
|
|
204
|
+
return {
|
|
205
|
+
probeId: probe.probeId,
|
|
206
|
+
requirementId: probe.requirementId,
|
|
207
|
+
status: 'NOT_EVALUATED',
|
|
208
|
+
sourceSection: probe.sourceSection,
|
|
209
|
+
evidenceRef: null,
|
|
210
|
+
workspaceIsolation,
|
|
211
|
+
failureClass,
|
|
212
|
+
adapterReason: reason,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function executePlan(workspace, plan) {
|
|
217
|
+
const errors = validatePlan(plan);
|
|
218
|
+
if (errors.length) return { schemaVersion: VERSION, adapter: ADAPTER, result: 'INVALID_PLAN', errors, probes: [] };
|
|
219
|
+
|
|
220
|
+
const discovered = discoverProject(workspace, plan);
|
|
221
|
+
if (!discovered.ok) {
|
|
222
|
+
return {
|
|
223
|
+
schemaVersion: VERSION,
|
|
224
|
+
adapter: ADAPTER,
|
|
225
|
+
result: 'NOT_EVALUATED',
|
|
226
|
+
discovery: discovered,
|
|
227
|
+
baseline: null,
|
|
228
|
+
probes: plan.probes.map(probe => notEvaluatedProbe(probe, discovered.reason)),
|
|
229
|
+
errors: [],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let baselineIsolation;
|
|
234
|
+
try {
|
|
235
|
+
baselineIsolation = copyWorkspace(workspace);
|
|
236
|
+
const baselineRun = runScript(baselineIsolation.workspace, plan);
|
|
237
|
+
const baseline = {
|
|
238
|
+
status: baselineRun.kind === 'exit' && baselineRun.exitCode === 0 ? 'PASS' : 'FAIL',
|
|
239
|
+
exitCode: baselineRun.exitCode,
|
|
240
|
+
failureClass: baselineRun.kind === 'exit' ? (baselineRun.exitCode === 0 ? 'none' : 'baseline-test') : 'infrastructure',
|
|
241
|
+
evidenceRef: `baseline:output-sha256:${baselineRun.outputHash}`,
|
|
242
|
+
workspaceIsolation: 'isolated',
|
|
243
|
+
};
|
|
244
|
+
if (baseline.status !== 'PASS') {
|
|
245
|
+
return {
|
|
246
|
+
schemaVersion: VERSION,
|
|
247
|
+
adapter: ADAPTER,
|
|
248
|
+
result: 'NOT_EVALUATED',
|
|
249
|
+
discovery: discovered,
|
|
250
|
+
baseline,
|
|
251
|
+
probes: plan.probes.map(probe => notEvaluatedProbe(probe, 'baseline-not-green')),
|
|
252
|
+
errors: [],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
} catch (error) {
|
|
256
|
+
const reason = String(error.message || error);
|
|
257
|
+
return {
|
|
258
|
+
schemaVersion: VERSION,
|
|
259
|
+
adapter: ADAPTER,
|
|
260
|
+
result: 'NOT_EVALUATED',
|
|
261
|
+
discovery: discovered,
|
|
262
|
+
baseline: {
|
|
263
|
+
status: 'NOT_EVALUATED',
|
|
264
|
+
evidenceRef: null,
|
|
265
|
+
workspaceIsolation: 'not-isolated',
|
|
266
|
+
failureClass: 'infrastructure',
|
|
267
|
+
adapterReason: reason,
|
|
268
|
+
},
|
|
269
|
+
probes: plan.probes.map(probe =>
|
|
270
|
+
notEvaluatedProbe(probe, reason, 'infrastructure', 'not-isolated')),
|
|
271
|
+
errors: [],
|
|
272
|
+
};
|
|
273
|
+
} finally {
|
|
274
|
+
if (baselineIsolation?.root) fs.rmSync(baselineIsolation.root, { recursive: true, force: true });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const results = [];
|
|
278
|
+
for (const probe of plan.probes) {
|
|
279
|
+
let isolated;
|
|
280
|
+
try {
|
|
281
|
+
isolated = copyWorkspace(workspace);
|
|
282
|
+
const applied = applyProbe(isolated.workspace, probe);
|
|
283
|
+
const run = runScript(isolated.workspace, plan, applied.env);
|
|
284
|
+
if (run.kind !== 'exit') {
|
|
285
|
+
results.push(notEvaluatedProbe(probe, run.kind));
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (run.exitCode === 0) {
|
|
289
|
+
results.push({
|
|
290
|
+
probeId: probe.probeId,
|
|
291
|
+
requirementId: probe.requirementId,
|
|
292
|
+
status: 'SURVIVED',
|
|
293
|
+
sourceSection: probe.sourceSection,
|
|
294
|
+
evidenceRef: evidenceRef(probe, run),
|
|
295
|
+
workspaceIsolation: 'isolated',
|
|
296
|
+
failureClass: 'none',
|
|
297
|
+
adapterReason: 'project-test-suite-remained-green',
|
|
298
|
+
});
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (run.combined.includes(probe.expectedFailureContains)) {
|
|
302
|
+
results.push({
|
|
303
|
+
probeId: probe.probeId,
|
|
304
|
+
requirementId: probe.requirementId,
|
|
305
|
+
status: 'KILLED',
|
|
306
|
+
sourceSection: probe.sourceSection,
|
|
307
|
+
evidenceRef: evidenceRef(probe, run),
|
|
308
|
+
workspaceIsolation: 'isolated',
|
|
309
|
+
failureClass: 'contract',
|
|
310
|
+
adapterReason: 'expected-observable-contract-failure',
|
|
311
|
+
});
|
|
312
|
+
} else {
|
|
313
|
+
results.push({
|
|
314
|
+
probeId: probe.probeId,
|
|
315
|
+
requirementId: probe.requirementId,
|
|
316
|
+
status: 'INVALID',
|
|
317
|
+
sourceSection: probe.sourceSection,
|
|
318
|
+
evidenceRef: evidenceRef(probe, run),
|
|
319
|
+
workspaceIsolation: 'isolated',
|
|
320
|
+
failureClass: 'unknown',
|
|
321
|
+
adapterReason: 'test-failed-without-expected-contract-observable',
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
} catch (error) {
|
|
325
|
+
results.push(notEvaluatedProbe(
|
|
326
|
+
probe,
|
|
327
|
+
String(error.message || error),
|
|
328
|
+
'infrastructure',
|
|
329
|
+
isolated ? 'isolated' : 'not-isolated',
|
|
330
|
+
));
|
|
331
|
+
} finally {
|
|
332
|
+
if (isolated?.root) fs.rmSync(isolated.root, { recursive: true, force: true });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
schemaVersion: VERSION,
|
|
338
|
+
adapter: ADAPTER,
|
|
339
|
+
result: results.every(probe => probe.status === 'KILLED') ? 'PROTECTED' :
|
|
340
|
+
results.some(probe => probe.status === 'SURVIVED') ? 'SURVIVED' : 'NOT_EVALUATED',
|
|
341
|
+
discovery: discovered,
|
|
342
|
+
baseline: { status: 'PASS', workspaceIsolation: 'isolated' },
|
|
343
|
+
probes: results,
|
|
344
|
+
errors: [],
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function parseArgs(argv) {
|
|
349
|
+
const args = { workspace: null, plan: null, output: null };
|
|
350
|
+
for (let i = 0; i < argv.length; i++) {
|
|
351
|
+
if (argv[i] === '--workspace') args.workspace = argv[++i];
|
|
352
|
+
else if (argv[i] === '--plan') args.plan = argv[++i];
|
|
353
|
+
else if (argv[i] === '--output') args.output = argv[++i];
|
|
354
|
+
else throw new Error('unknown argument: ' + argv[i]);
|
|
355
|
+
}
|
|
356
|
+
if (!args.workspace || !args.plan) throw new Error('Usage: node node-probe-adapter.js --workspace <project> --plan <probe-plan.json> [--output report.json]');
|
|
357
|
+
return args;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function main() {
|
|
361
|
+
try {
|
|
362
|
+
const args = parseArgs(process.argv.slice(2));
|
|
363
|
+
const workspace = path.resolve(args.workspace);
|
|
364
|
+
const plan = JSON.parse(fs.readFileSync(path.resolve(args.plan), 'utf8'));
|
|
365
|
+
const report = executePlan(workspace, plan);
|
|
366
|
+
const json = JSON.stringify(report, null, 2) + '\n';
|
|
367
|
+
if (args.output) {
|
|
368
|
+
fs.mkdirSync(path.dirname(path.resolve(args.output)), { recursive: true });
|
|
369
|
+
fs.writeFileSync(path.resolve(args.output), json, 'utf8');
|
|
370
|
+
}
|
|
371
|
+
process.stdout.write(json);
|
|
372
|
+
process.exitCode = report.result === 'PROTECTED' ? 0 : report.result === 'SURVIVED' ? 1 : 2;
|
|
373
|
+
} catch (error) {
|
|
374
|
+
console.error('[Contract Probe Adapter] ' + error.message);
|
|
375
|
+
process.exitCode = 2;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (require.main === module) main();
|
|
380
|
+
|
|
381
|
+
module.exports = {
|
|
382
|
+
ADAPTER,
|
|
383
|
+
VERSION,
|
|
384
|
+
applyProbe,
|
|
385
|
+
discoverProject,
|
|
386
|
+
executePlan,
|
|
387
|
+
runScript,
|
|
388
|
+
safeRelative,
|
|
389
|
+
validatePlan,
|
|
390
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "harness-everything",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.15.0",
|
|
5
5
|
"description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Miya Daniel",
|
|
@@ -4,7 +4,7 @@ description: "Run objective verification gates (build, types, lint, tests, secur
|
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
6
|
author: Miya Daniel
|
|
7
|
-
version: 0.
|
|
7
|
+
version: 0.15.0
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Verification Loop
|
|
@@ -41,7 +41,7 @@ and rerun from the first affected gate.
|
|
|
41
41
|
4. Tests and coverage: `npm run test -- --coverage`.
|
|
42
42
|
5. Security scan: secrets and stray logs.
|
|
43
43
|
6. Diff review: `git diff --stat` and unintended-change checks.
|
|
44
|
-
7. Contract reconciliation when the work cites `REQ-*` / ADR/spec/ticket lineage or a contract-integrity trace exists: run the strict `<this-skill-dir>/scripts/contract-integrity-audit.js` gate with current #58 evidence. `AUDIT`, `NOT_EVALUATED`, stale source binding, or drift is **not ready** for delivery.
|
|
44
|
+
7. Contract reconciliation when the work cites `REQ-*` / ADR/spec/ticket lineage or a contract-integrity trace exists: run the strict `<this-skill-dir>/scripts/contract-integrity-audit.js` gate with current #58 evidence and `--workspace <workspace>` so traced source/test/implementation artifacts are fingerprinted. `AUDIT`, `NOT_EVALUATED`, stale source binding, or drift is **not ready** for delivery.
|
|
45
45
|
8. Fill `<this-skill-dir>/templates/verification-report.template.md` with evidence.
|
|
46
46
|
|
|
47
47
|
Deep dive: `<this-skill-dir>/references/verification-phases.md`
|
package/plugins/harness-everything/skills/verification-loop/references/verification-phases.md
CHANGED
|
@@ -110,11 +110,19 @@ This phase is applicable when any of the following is true:
|
|
|
110
110
|
Use the current trace plus current #58 TDD evidence:
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
|
-
node "<this-skill-dir>/scripts/contract-integrity-audit.js" <trace.json> --tdd-evidence <tdd-evidence.json> --output <report.json> --markdown <report.md>
|
|
113
|
+
node "<this-skill-dir>/scripts/contract-integrity-audit.js" <trace.json> --tdd-evidence <tdd-evidence.json> --workspace <workspace> --output <report.json> --markdown <report.md>
|
|
114
114
|
```
|
|
115
115
|
|
|
116
116
|
Delivery requires strict **PASS**. Treat `FAIL`, `AUDIT`, `NOT_EVALUATED`, source conflicts/defects, stale spec/test/implementation lineage, surviving required probes, and audit-only output as **NOT READY**. Do not downgrade the gate merely because build/tests are otherwise green.
|
|
117
117
|
|
|
118
|
+
If an existing JSON report is being reused rather than regenerated immediately, verify its content fingerprints first:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
node "<this-skill-dir>/scripts/contract-integrity-audit.js" <trace.json> --tdd-evidence <tdd-evidence.json> --workspace <workspace> --verify-fresh <report.json>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Any trace, #58 evidence, spec, test, ticket, ADR, or implementation artifact change makes the prior report stale and unable to authorize completion.
|
|
125
|
+
|
|
118
126
|
If this phase is not applicable, record `NOT_APPLICABLE` with a one-line reason. Never use `NOT_APPLICABLE` to hide known requirement lineage.
|
|
119
127
|
|
|
120
128
|
## Output Format
|
|
@@ -71,8 +71,11 @@ syncTree(path.join(ROOT, 'scripts', 'lib', 'advisory-text.js'), packageAdvisoryT
|
|
|
71
71
|
// Contract-integrity is shared runtime, not a directly-routed 27th skill.
|
|
72
72
|
// Package it under the plugin root and rewrite its one canonical TDD import
|
|
73
73
|
// to the packaged skill tree.
|
|
74
|
-
const
|
|
74
|
+
const packageContractRoot = path.join(pluginRoot, 'contract-integrity');
|
|
75
|
+
const packageContractAudit = path.join(packageContractRoot, 'scripts', 'audit.js');
|
|
75
76
|
syncTree(path.join(ROOT, 'contract-integrity', 'scripts', 'audit.js'), packageContractAudit);
|
|
77
|
+
syncTree(path.join(ROOT, 'contract-integrity', 'scripts', 'node-probe-adapter.js'), path.join(packageContractRoot, 'scripts', 'node-probe-adapter.js'));
|
|
78
|
+
syncTree(path.join(ROOT, 'contract-integrity', 'ADAPTERS.md'), path.join(packageContractRoot, 'ADAPTERS.md'));
|
|
76
79
|
const contractAuditSource = fs.readFileSync(packageContractAudit, 'utf8');
|
|
77
80
|
fs.writeFileSync(
|
|
78
81
|
packageContractAudit,
|
|
@@ -4,7 +4,7 @@ description: "Run objective verification gates (build, types, lint, tests, secur
|
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
6
|
author: Miya Daniel
|
|
7
|
-
version: 0.
|
|
7
|
+
version: 0.15.0
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Verification Loop
|
|
@@ -41,7 +41,7 @@ and rerun from the first affected gate.
|
|
|
41
41
|
4. Tests and coverage: `npm run test -- --coverage`.
|
|
42
42
|
5. Security scan: secrets and stray logs.
|
|
43
43
|
6. Diff review: `git diff --stat` and unintended-change checks.
|
|
44
|
-
7. Contract reconciliation when the work cites `REQ-*` / ADR/spec/ticket lineage or a contract-integrity trace exists: run the strict `<this-skill-dir>/scripts/contract-integrity-audit.js` gate with current #58 evidence. `AUDIT`, `NOT_EVALUATED`, stale source binding, or drift is **not ready** for delivery.
|
|
44
|
+
7. Contract reconciliation when the work cites `REQ-*` / ADR/spec/ticket lineage or a contract-integrity trace exists: run the strict `<this-skill-dir>/scripts/contract-integrity-audit.js` gate with current #58 evidence and `--workspace <workspace>` so traced source/test/implementation artifacts are fingerprinted. `AUDIT`, `NOT_EVALUATED`, stale source binding, or drift is **not ready** for delivery.
|
|
45
45
|
8. Fill `<this-skill-dir>/templates/verification-report.template.md` with evidence.
|
|
46
46
|
|
|
47
47
|
Deep dive: `<this-skill-dir>/references/verification-phases.md`
|
|
@@ -110,11 +110,19 @@ This phase is applicable when any of the following is true:
|
|
|
110
110
|
Use the current trace plus current #58 TDD evidence:
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
|
-
node "<this-skill-dir>/scripts/contract-integrity-audit.js" <trace.json> --tdd-evidence <tdd-evidence.json> --output <report.json> --markdown <report.md>
|
|
113
|
+
node "<this-skill-dir>/scripts/contract-integrity-audit.js" <trace.json> --tdd-evidence <tdd-evidence.json> --workspace <workspace> --output <report.json> --markdown <report.md>
|
|
114
114
|
```
|
|
115
115
|
|
|
116
116
|
Delivery requires strict **PASS**. Treat `FAIL`, `AUDIT`, `NOT_EVALUATED`, source conflicts/defects, stale spec/test/implementation lineage, surviving required probes, and audit-only output as **NOT READY**. Do not downgrade the gate merely because build/tests are otherwise green.
|
|
117
117
|
|
|
118
|
+
If an existing JSON report is being reused rather than regenerated immediately, verify its content fingerprints first:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
node "<this-skill-dir>/scripts/contract-integrity-audit.js" <trace.json> --tdd-evidence <tdd-evidence.json> --workspace <workspace> --verify-fresh <report.json>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Any trace, #58 evidence, spec, test, ticket, ADR, or implementation artifact change makes the prior report stale and unable to authorize completion.
|
|
125
|
+
|
|
118
126
|
If this phase is not applicable, record `NOT_APPLICABLE` with a one-line reason. Never use `NOT_APPLICABLE` to hide known requirement lineage.
|
|
119
127
|
|
|
120
128
|
## Output Format
|