harness-everything 0.35.0 → 0.36.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/.github/workflows/ci.yml +2 -0
- package/AGENTS.md +3 -0
- package/CHANGELOG.md +17 -0
- package/ci/mechanism-36-verify-timeout.test.js +534 -0
- package/ci/mechanism-37-portability-lint.test.js +71 -0
- package/ci/mechanism-38-verify-all.test.js +112 -0
- package/ci/mechanism-39-opencode-verify-outcomes.test.js +374 -0
- package/ci/portability-lint.js +288 -0
- package/docs/repository-contract.md +1 -1
- package/opencode-plugin/README.md +33 -2
- package/opencode-plugin/hooks/verify.js +87 -11
- package/opencode-plugin/index.mjs +216 -25
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +3 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/sbom.cdx.json +7 -7
- package/scripts/verify-all.js +314 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Local CI-mirror gate runner.
|
|
6
|
+
*
|
|
7
|
+
* AGENTS.md lists the gate commands that cover the same contract categories
|
|
8
|
+
* as CI; recent PR history shows runs where a gate category was skipped
|
|
9
|
+
* locally and failed only after push (PR #301 consistency/runtime-floor,
|
|
10
|
+
* PR #327 Windows self-regression). This script runs the same set in CI
|
|
11
|
+
* order, from the working tree or a clean detached checkout of HEAD, and
|
|
12
|
+
* prints a Markdown table that is paste-ready for PR evidence.
|
|
13
|
+
*
|
|
14
|
+
* Every known gate always appears in the report. A gate that did not run is
|
|
15
|
+
* UNCHECKED with its reason (fast subset, skipped by flag, tool unavailable)
|
|
16
|
+
* and never counts as passing evidence.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* npm run verify:all # full gate set, current tree
|
|
20
|
+
* npm run verify:all -- --fast # hermetic + mutations + consistency + portability
|
|
21
|
+
* npm run verify:all -- --clean # all gates in a fresh detached worktree of HEAD
|
|
22
|
+
* npm run verify:all -- --node22 # add the Node 22 execution smoke-test lane (npx node@22)
|
|
23
|
+
* npm run verify:all -- --clean --no-install # --clean without npm ci in the worktree
|
|
24
|
+
* npm run verify:all -- --no-waza # skip the optional waza readiness gate
|
|
25
|
+
*
|
|
26
|
+
* Exit code is 1 when any executed gate fails.
|
|
27
|
+
*
|
|
28
|
+
* Process launching: `git` always runs with shell:false and structured
|
|
29
|
+
* argument vectors, so worktree paths with spaces survive on every OS. On
|
|
30
|
+
* Windows `npm`/`npx`/`waza` may be `.cmd` shims, which Node refuses to
|
|
31
|
+
* spawn without a shell; those are launched through the shell with only
|
|
32
|
+
* fixed, space-free argument tokens (scripts/ci/mechanism-38-* covers the
|
|
33
|
+
* split).
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const fs = require('fs');
|
|
37
|
+
const os = require('os');
|
|
38
|
+
const path = require('path');
|
|
39
|
+
const { spawnSync } = require('child_process');
|
|
40
|
+
|
|
41
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
42
|
+
|
|
43
|
+
const FULL_PLAN = [
|
|
44
|
+
{ label: 'self-regression (hermetic + mechanism + routing contracts)', command: ['npm', 'test'], fast: true },
|
|
45
|
+
{ label: 'mutation checks', command: ['npm', 'run', 'test:mutations'], fast: true },
|
|
46
|
+
{ label: 'consistency (docs, manifests, repo contract, prompt contracts)', command: ['npm', 'run', 'test:consistency'], fast: true },
|
|
47
|
+
{ label: 'references resolve', command: ['npm', 'run', 'test:references'], fast: false },
|
|
48
|
+
{ label: 'release consistency', command: ['npm', 'run', 'test:release'], fast: false },
|
|
49
|
+
{ label: 'skill description collision', command: ['npm', 'run', 'test:collision'], fast: false },
|
|
50
|
+
{ label: 'skill routing coverage', command: ['npm', 'run', 'test:routing:skills'], fast: false },
|
|
51
|
+
{ label: 'routing invariants', command: ['npm', 'run', 'test:routing:invariants'], fast: false },
|
|
52
|
+
{ label: 'openai plugin package', command: ['npm', 'run', 'test:plugin:openai'], fast: false },
|
|
53
|
+
{ label: 'openai public submission', command: ['npm', 'run', 'test:plugin:submission'], fast: false },
|
|
54
|
+
{ label: 'platform compatibility matrix', command: ['npm', 'run', 'test:platform:compatibility'], fast: false },
|
|
55
|
+
{ label: 'test-source portability', command: ['npm', 'run', 'test:portability'], fast: true },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const NODE22_PLAN = [
|
|
59
|
+
{ label: 'node22 lane: self-regression execution smoke test', command: ['npx', '-y', 'node@22', 'self-evolve/scripts/self-regression.js'] },
|
|
60
|
+
{ label: 'node22 lane: installer roundtrip execution smoke test', command: ['npx', '-y', 'node@22', 'ci/installer-roundtrip.test.js'] },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
// CI's runtime-floor job installs dependencies under Node 22 before running
|
|
64
|
+
// the suite. Locally the smoke tests execute under Node 22 but use the
|
|
65
|
+
// current host's node_modules, so dependency-install compatibility stays
|
|
66
|
+
// explicitly UNCHECKED instead of being implied.
|
|
67
|
+
const NODE22_UNVERIFIED = {
|
|
68
|
+
label: 'node22 lane: dependency install (npm ci under Node 22)',
|
|
69
|
+
reason: 'covered by the CI runtime-floor job; not replicated locally',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const WAZA_ENTRY = {
|
|
73
|
+
label: 'waza readiness (optional)',
|
|
74
|
+
command: ['npm', 'run', 'waza:check'],
|
|
75
|
+
tool: 'waza',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function usage() {
|
|
79
|
+
console.log('Usage: npm run verify:all [-- --fast] [-- --clean] [-- --node22] [-- --no-install] [-- --no-waza]');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function parseArgs(argv) {
|
|
83
|
+
const options = { fast: false, clean: false, node22: false, install: true, waza: true };
|
|
84
|
+
for (const arg of argv) {
|
|
85
|
+
if (arg === '--fast') options.fast = true;
|
|
86
|
+
else if (arg === '--clean') options.clean = true;
|
|
87
|
+
else if (arg === '--node22') options.node22 = true;
|
|
88
|
+
else if (arg === '--no-install') options.install = false;
|
|
89
|
+
else if (arg === '--no-waza') options.waza = false;
|
|
90
|
+
else if (arg === '--help' || arg === '-h') {
|
|
91
|
+
usage();
|
|
92
|
+
process.exit(0);
|
|
93
|
+
} else {
|
|
94
|
+
console.error(`Unknown argument: ${arg}`);
|
|
95
|
+
usage();
|
|
96
|
+
process.exit(2);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return options;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Windows may resolve npm/npx/waza as .cmd shims that Node only spawns with
|
|
103
|
+
// a shell. They receive fixed, space-free tokens only; git never needs this.
|
|
104
|
+
const SHELL_TOOLS = new Set(['npm', 'npx', 'waza']);
|
|
105
|
+
|
|
106
|
+
function needsShell(cmd, platform = process.platform) {
|
|
107
|
+
return platform === 'win32' && SHELL_TOOLS.has(cmd);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function spawnTool(cmd, args, options) {
|
|
111
|
+
return spawnSync(cmd, args, { ...options, shell: needsShell(cmd) });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function run(command, cwd, env) {
|
|
115
|
+
const [cmd, ...args] = command;
|
|
116
|
+
const started = Date.now();
|
|
117
|
+
const result = spawnTool(cmd, args, {
|
|
118
|
+
cwd,
|
|
119
|
+
stdio: 'inherit',
|
|
120
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
121
|
+
});
|
|
122
|
+
const seconds = (Date.now() - started) / 1000;
|
|
123
|
+
if (result.error) {
|
|
124
|
+
console.error(`spawn failed: ${cmd} — ${result.error.message}`);
|
|
125
|
+
return { status: null, seconds };
|
|
126
|
+
}
|
|
127
|
+
return { status: result.status, seconds };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function capture(command, cwd) {
|
|
131
|
+
const [cmd, ...args] = command;
|
|
132
|
+
return spawnTool(cmd, args, { cwd, encoding: 'utf8' });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function runGit(args, cwd) {
|
|
136
|
+
return spawnSync('git', args, { cwd, encoding: 'utf8', shell: false });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function formatSeconds(value) {
|
|
140
|
+
return `${value.toFixed(1)}s`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function wazaEnv() {
|
|
144
|
+
const probe = capture(['waza', '--version']);
|
|
145
|
+
if (probe.status === 0) return {};
|
|
146
|
+
const local = path.join(os.homedir(), 'bin', process.platform === 'win32' ? 'waza.exe' : 'waza');
|
|
147
|
+
if (fs.existsSync(local)) return { WAZA_BIN: local };
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildPlan(options) {
|
|
152
|
+
const entries = FULL_PLAN.map(entry => ({
|
|
153
|
+
...entry,
|
|
154
|
+
enabled: options.fast ? entry.fast : true,
|
|
155
|
+
reason: entry.fast ? null : 'not in the --fast subset',
|
|
156
|
+
}));
|
|
157
|
+
entries.push(...NODE22_PLAN.map(entry => ({
|
|
158
|
+
...entry,
|
|
159
|
+
enabled: options.node22,
|
|
160
|
+
reason: options.node22 ? null : 'not requested; add --node22',
|
|
161
|
+
})));
|
|
162
|
+
entries.push({ ...NODE22_UNVERIFIED, enabled: false, alwaysUnchecked: true });
|
|
163
|
+
entries.push({
|
|
164
|
+
...WAZA_ENTRY,
|
|
165
|
+
enabled: options.waza,
|
|
166
|
+
reason: options.waza ? null : 'skipped by --no-waza',
|
|
167
|
+
});
|
|
168
|
+
return entries;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function prepareCleanRoot(install) {
|
|
172
|
+
const head = runGit(['rev-parse', 'HEAD'], ROOT).stdout.trim();
|
|
173
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-verify-all-'));
|
|
174
|
+
const add = runGit(['worktree', 'add', '--detach', dir, head], ROOT);
|
|
175
|
+
if (add.status !== 0) {
|
|
176
|
+
throw new Error(`git worktree add failed: ${add.stderr || add.stdout}`);
|
|
177
|
+
}
|
|
178
|
+
if (install) {
|
|
179
|
+
console.log('\nInstalling dependencies in clean worktree (npm ci --ignore-scripts)...');
|
|
180
|
+
const ci = run(['npm', 'ci', '--ignore-scripts'], dir);
|
|
181
|
+
if (ci.status !== 0) {
|
|
182
|
+
runGit(['worktree', 'remove', '--force', dir], ROOT);
|
|
183
|
+
throw new Error('npm ci failed in the clean worktree');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { dir, head };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function removeCleanRoot(dir) {
|
|
190
|
+
const removed = runGit(['worktree', 'remove', '--force', dir], ROOT);
|
|
191
|
+
if (removed.status !== 0) {
|
|
192
|
+
console.error(`warning: could not remove clean worktree ${dir}: ${removed.stderr || removed.stdout}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function formatMode(options) {
|
|
197
|
+
return options.fast ? 'fast subset' : 'full gate set';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function formatFlags(options) {
|
|
201
|
+
const flags = [];
|
|
202
|
+
if (options.fast) flags.push('--fast');
|
|
203
|
+
if (options.clean) flags.push('--clean');
|
|
204
|
+
if (options.node22) flags.push('--node22');
|
|
205
|
+
if (!options.install) flags.push('--no-install');
|
|
206
|
+
if (!options.waza) flags.push('--no-waza');
|
|
207
|
+
return flags.length > 0 ? flags.join(' ') : '(defaults)';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function renderReport(rows, meta) {
|
|
211
|
+
const lines = [];
|
|
212
|
+
lines.push('## Local gate run — paste-ready evidence');
|
|
213
|
+
lines.push(
|
|
214
|
+
`HEAD \`${meta.head}\` · node ${meta.node} · ${meta.platform}` +
|
|
215
|
+
`${meta.clean ? ' · clean detached worktree' : ''} · mode: ${meta.mode} · flags: ${meta.flags}`
|
|
216
|
+
);
|
|
217
|
+
lines.push('');
|
|
218
|
+
lines.push('| # | Gate | Result | Time |');
|
|
219
|
+
lines.push('| --- | --- | --- | --- |');
|
|
220
|
+
rows.forEach((row, index) => {
|
|
221
|
+
const result = row.status === 'UNCHECKED' ? `UNCHECKED (${row.reason})` : row.status;
|
|
222
|
+
const time = row.seconds === null || row.seconds === undefined ? '—' : formatSeconds(row.seconds);
|
|
223
|
+
lines.push(`| ${index + 1} | ${row.label} | ${result} | ${time} |`);
|
|
224
|
+
});
|
|
225
|
+
const failures = rows.filter(row => row.status === 'FAIL').length;
|
|
226
|
+
const unchecked = rows.filter(row => row.status === 'UNCHECKED').length;
|
|
227
|
+
const passed = rows.filter(row => row.status === 'PASS').length;
|
|
228
|
+
lines.push('');
|
|
229
|
+
lines.push(`Summary: ${passed} PASS, ${failures} FAIL, ${unchecked} UNCHECKED.`);
|
|
230
|
+
return { text: lines.join('\n'), failures, unchecked, passed };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function probeWazaEntry(entry) {
|
|
234
|
+
if (entry.tool !== 'waza' || !entry.enabled) return entry;
|
|
235
|
+
const env = wazaEnv();
|
|
236
|
+
if (env) return { ...entry, env };
|
|
237
|
+
return { ...entry, enabled: false, reason: 'waza not found on PATH or ~/bin' };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function main() {
|
|
241
|
+
const options = parseArgs(process.argv.slice(2));
|
|
242
|
+
const head = runGit(['rev-parse', 'HEAD'], ROOT).stdout.trim();
|
|
243
|
+
|
|
244
|
+
console.log('=== 🚦 Harness local gate runner (CI mirror) ===');
|
|
245
|
+
console.log(`Repository: ${ROOT}`);
|
|
246
|
+
console.log(`HEAD: ${head}`);
|
|
247
|
+
console.log(`Runtime: node ${process.version} on ${process.platform}`);
|
|
248
|
+
console.log(`Mode: ${formatMode(options)} · flags: ${formatFlags(options)}`);
|
|
249
|
+
|
|
250
|
+
const plan = buildPlan(options).map(probeWazaEntry);
|
|
251
|
+
|
|
252
|
+
let cwd = ROOT;
|
|
253
|
+
let clean = null;
|
|
254
|
+
if (options.clean) {
|
|
255
|
+
clean = prepareCleanRoot(options.install);
|
|
256
|
+
cwd = clean.dir;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const rows = [];
|
|
260
|
+
try {
|
|
261
|
+
let executed = 0;
|
|
262
|
+
const enabledCount = plan.filter(entry => entry.enabled).length;
|
|
263
|
+
for (const entry of plan) {
|
|
264
|
+
if (!entry.enabled) {
|
|
265
|
+
rows.push({ label: entry.label, status: 'UNCHECKED', reason: entry.reason, seconds: null });
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
executed += 1;
|
|
269
|
+
console.log(`\n▶ [${executed}/${enabledCount}] ${entry.label}`);
|
|
270
|
+
console.log(` $ ${entry.command.join(' ')}`);
|
|
271
|
+
const result = run(entry.command, cwd, entry.env);
|
|
272
|
+
rows.push({
|
|
273
|
+
label: entry.label,
|
|
274
|
+
status: result.status === 0 ? 'PASS' : 'FAIL',
|
|
275
|
+
seconds: result.seconds,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
} finally {
|
|
279
|
+
if (clean) removeCleanRoot(clean.dir);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const report = renderReport(rows, {
|
|
283
|
+
head,
|
|
284
|
+
node: process.version,
|
|
285
|
+
platform: process.platform,
|
|
286
|
+
clean: Boolean(options.clean),
|
|
287
|
+
mode: formatMode(options),
|
|
288
|
+
flags: formatFlags(options),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
console.log(`\n${report.text}`);
|
|
292
|
+
|
|
293
|
+
if (report.failures > 0) {
|
|
294
|
+
process.exitCode = 1;
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (require.main === module) {
|
|
300
|
+
main();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = {
|
|
304
|
+
FULL_PLAN,
|
|
305
|
+
NODE22_PLAN,
|
|
306
|
+
NODE22_UNVERIFIED,
|
|
307
|
+
buildPlan,
|
|
308
|
+
needsShell,
|
|
309
|
+
renderReport,
|
|
310
|
+
runGit,
|
|
311
|
+
formatMode,
|
|
312
|
+
formatFlags,
|
|
313
|
+
parseArgs,
|
|
314
|
+
};
|