ruvnet-brain 4.3.9 → 4.3.11
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/README.md +26 -9
- package/bin/install.mjs +646 -366
- package/bin/nightly-refresh.mjs +115 -0
- package/kb/brain-profile.mjs +118 -32
- package/kb/lifecycle-evidence-retention.mjs +237 -0
- package/kb/refresh-run.mjs +367 -0
- package/kb/retrieval-result.mjs +39 -0
- package/kb/update-storage-transaction.mjs +439 -0
- package/package.json +12 -3
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +2 -165
- package/plugin/hooks/hook-contracts.json +4 -65
- package/plugin/hooks/hooks.json +2 -208
- package/plugin/scripts/adr-currency-gate.mjs +29 -19
- package/plugin/scripts/capability-registry.mjs +10 -83
- package/plugin/scripts/codex-hook-adapter.mjs +62 -9
- package/plugin/scripts/codex-hook-wrapper.mjs +19 -1
- package/plugin/scripts/continuation-gate.mjs +30 -28
- package/plugin/scripts/continuation-objective.mjs +34 -0
- package/plugin/scripts/development-maintenance.mjs +49 -0
- package/plugin/scripts/hook-shim.mjs +3 -0
- package/plugin/scripts/learn-flush.mjs +6 -1
- package/plugin/scripts/lesson-gate.mjs +5 -2
- package/plugin/scripts/lesson-presentation.mjs +5 -0
- package/plugin/scripts/md-stamp.mjs +109 -6
- package/plugin/scripts/memory-doctor.mjs +3 -7
- package/plugin/scripts/nightly-controller.mjs +9 -27
- package/plugin/scripts/nightly-scheduler.mjs +494 -0
- package/plugin/scripts/project-progression-contract.mjs +4 -4
- package/plugin/scripts/project-progression-store.mjs +35 -6
- package/plugin/scripts/ruflo-bin.mjs +20 -0
- package/plugin/scripts/session-snapshot-contract.mjs +6 -1
- package/plugin/scripts/version-bump-gate.sh +14 -2
- package/plugin/skills/release-proof/SKILL.md +6 -2
- package/plugin/skills/release-proof/references/receipt-contract.md +31 -2
- package/plugin/skills/release-proof/scripts/release-proof.mjs +11 -2
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +15 -23
- package/scripts/build-bundle.mjs +8 -0
- package/scripts/build-primer.mjs +2 -3
- package/scripts/candidate-host-evidence.mjs +59 -41
- package/scripts/ci/build-fixture-kb.mjs +59 -2
- package/scripts/ci/mutate-hook-timeout.mjs +24 -47
- package/scripts/ci/stranger-scenario.mjs +13 -28
- package/scripts/claims-verify.mjs +219 -21
- package/scripts/console-engine.mjs +4 -18
- package/scripts/console-runtime-identity.mjs +6 -0
- package/scripts/development-maintenance.mjs +47 -0
- package/scripts/development-push-check.mjs +35 -0
- package/scripts/distill-project.mjs +24 -47
- package/scripts/doc-currency.mjs +91 -19
- package/scripts/git-hooks/pre-push +4 -162
- package/scripts/health-repair.mjs +22 -4
- package/scripts/hook-retirement-check.mjs +22 -0
- package/scripts/host-install-matrix.mjs +125 -61
- package/scripts/integration-evidence.mjs +11 -6
- package/scripts/learning-replay-fixture.mjs +23 -2
- package/scripts/nightly-package-observation.mjs +67 -0
- package/scripts/nightly-two-run-proof.mjs +489 -0
- package/scripts/nightly-watchdog.mjs +17 -2
- package/scripts/npm-invocation.mjs +20 -0
- package/scripts/prepublication-evidence.mjs +42 -6
- package/scripts/primer-grounding.mjs +20 -0
- package/scripts/product-integrity-contract.mjs +22 -2
- package/scripts/public-verification-abandon.mjs +101 -0
- package/scripts/public-verification-aggregate.mjs +61 -19
- package/scripts/public-verification-finalizer.mjs +8 -2
- package/scripts/public-verification-lane.mjs +64 -8
- package/scripts/publication-receipt.mjs +156 -50
- package/scripts/published-surface-probe.mjs +10 -2
- package/scripts/qa-contract.mjs +57 -0
- package/scripts/qa-lanes.mjs +40 -0
- package/scripts/qa-runner.mjs +54 -64
- package/scripts/qe/ux-suite.mjs +10 -34
- package/scripts/qualified-candidate-check.mjs +124 -0
- package/scripts/release-abort-stale.mjs +2 -2
- package/scripts/release-projection.mjs +20 -1
- package/scripts/release-qualification-contract.mjs +87 -0
- package/scripts/release-qualification.mjs +135 -0
- package/scripts/release-transaction.mjs +81 -1
- package/scripts/remedy-registry.mjs +6 -13
- package/scripts/retrieval-canary.mjs +91 -18
- package/scripts/selfcheck.mjs +12 -4
- package/scripts/snapshot-freshness.mjs +58 -0
- package/scripts/stack-sync.mjs +41 -43
- package/scripts/staged-host-verifier.mjs +63 -14
- package/scripts/wired-check.mjs +31 -2
package/scripts/stack-sync.mjs
CHANGED
|
@@ -186,13 +186,15 @@ export function listInstalledPlugins(pluginsDir = PLUGINS_DIR) {
|
|
|
186
186
|
// not equality" discipline as the rest of this file. Fall back to the manifest's own version field
|
|
187
187
|
// if the on-disk plugin.json is unreadable, so a present plugin is NEVER reported "not installed".
|
|
188
188
|
let installed = null;
|
|
189
|
+
const instances = [];
|
|
189
190
|
for (const rec of Array.isArray(records) ? records : []) {
|
|
190
191
|
const v = (rec && rec.installPath ? pluginVersion(rec.installPath) : null)
|
|
191
192
|
|| (rec && rec.version && rec.version !== 'unknown' ? rec.version : null);
|
|
193
|
+
instances.push({ scope: rec?.scope || 'unknown', installPath: rec?.installPath || null, version: v, readable: Boolean(rec?.installPath && pluginVersion(rec.installPath)) });
|
|
192
194
|
if (!v) continue;
|
|
193
195
|
if (installed === null || cmpVersion(v, installed) > 0) installed = v;
|
|
194
196
|
}
|
|
195
|
-
out.push({ name, installed, source: 'plugin', marketplace });
|
|
197
|
+
out.push({ name, installed, source: 'plugin', marketplace, instances });
|
|
196
198
|
}
|
|
197
199
|
return out;
|
|
198
200
|
}
|
|
@@ -209,15 +211,8 @@ function listInstalled({ lib = GLOBAL_LIB, pluginsDir = PLUGINS_DIR } = {}) {
|
|
|
209
211
|
}
|
|
210
212
|
};
|
|
211
213
|
scan(lib);
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
// npm dist-tags. A plugin-only tool is added, so it can never be reported "not installed".
|
|
215
|
-
const seen = new Set(out.map((r) => r.name));
|
|
216
|
-
for (const p of listInstalledPlugins(pluginsDir)) {
|
|
217
|
-
if (seen.has(p.name)) continue;
|
|
218
|
-
seen.add(p.name);
|
|
219
|
-
out.push(p);
|
|
220
|
-
}
|
|
214
|
+
// Preserve both distribution sources; one must not hide the other.
|
|
215
|
+
out.push(...listInstalledPlugins(pluginsDir));
|
|
221
216
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
222
217
|
}
|
|
223
218
|
|
|
@@ -258,14 +253,9 @@ export function findShadows(npxCache = NPX_CACHE, lib = GLOBAL_LIB) {
|
|
|
258
253
|
// exits) share ONE classification, never two that can drift.
|
|
259
254
|
export function classify(pkgs) {
|
|
260
255
|
return pkgs.map((p) => {
|
|
261
|
-
//
|
|
262
|
-
// is no npm dist-tag oracle for it (querying `npm view <plugin>` would compare against an unrelated
|
|
263
|
-
// package or 404), so we do not manufacture a drift signal. It is present and installed ⇒ CURRENT;
|
|
264
|
-
// a plugin is only BROKEN if we could not read any version at all. Either way it is COUNTED and is
|
|
265
|
-
// never reported "not installed". This also keeps the npm-registry "blind tool" guard scoped to
|
|
266
|
-
// real npm rows (see audit()/auditModel()).
|
|
256
|
+
// Installed records prove presence, not marketplace currency.
|
|
267
257
|
if (p.source === 'plugin') {
|
|
268
|
-
return { ...p, tag: 'plugin', target:
|
|
258
|
+
return { ...p, tag: 'plugin', target: null, state: p.installed ? 'INSTALLED_UNVERIFIED' : 'BROKEN', evidence: 'local install record only; marketplace revision not checked' };
|
|
269
259
|
}
|
|
270
260
|
const want = TAG_POLICY[p.name] || DEFAULT_TAG;
|
|
271
261
|
const tags = registryTags(p.name);
|
|
@@ -293,9 +283,7 @@ function audit() {
|
|
|
293
283
|
// inside the fix for it. Silence is not health.
|
|
294
284
|
const rows = classify(pkgs);
|
|
295
285
|
const unresolved = rows.filter((r) => r.state === 'UNRESOLVED');
|
|
296
|
-
//
|
|
297
|
-
// here would let a fully-unreachable registry hide behind a couple of installed plugins — the exact
|
|
298
|
-
// "blind tool reports health" bug this guard exists to kill, reintroduced. Denominator = npm rows.
|
|
286
|
+
// Registry reachability is measured only against npm rows; plugins remain unverified.
|
|
299
287
|
const npmRows = rows.filter((r) => r.source !== 'plugin');
|
|
300
288
|
if (npmRows.length && unresolved.length === npmRows.length) {
|
|
301
289
|
die(`could not reach the npm registry for ANY of ${npmRows.length} npm packages.\n` +
|
|
@@ -303,7 +291,7 @@ function audit() {
|
|
|
303
291
|
}
|
|
304
292
|
|
|
305
293
|
const shadows = findShadows();
|
|
306
|
-
return { rows, unresolved, shadows, stale: shadows.filter((s) => s.global && s.version
|
|
294
|
+
return { rows, unresolved, shadows, stale: shadows.filter((s) => s.global && isBehind(s.version, s.global)) };
|
|
307
295
|
}
|
|
308
296
|
|
|
309
297
|
// Non-exiting audit for embedders (the Onboarding Console). Same measurement as audit(), but returns
|
|
@@ -316,32 +304,44 @@ export function auditModel() {
|
|
|
316
304
|
const rows = classify(pkgs);
|
|
317
305
|
const unresolved = rows.filter((r) => r.state === 'UNRESOLVED');
|
|
318
306
|
const shadows = findShadows();
|
|
319
|
-
const stale = shadows.filter((s) => s.global && s.version
|
|
307
|
+
const stale = shadows.filter((s) => s.global && isBehind(s.version, s.global));
|
|
320
308
|
// Same npm-scoped guard as audit() (ISSUE #22): plugin rows never count toward "registry unreachable".
|
|
321
309
|
const npmRows = rows.filter((r) => r.source !== 'plugin');
|
|
322
310
|
const error = npmRows.length && unresolved.length === npmRows.length
|
|
323
311
|
? `could not reach the npm registry for any of ${npmRows.length} npm packages` : null;
|
|
324
|
-
return { rows, unresolved, shadows, stale, error };
|
|
312
|
+
return { rows, unresolved, shadows, stale, error, summary: summarizeAudit({ rows, shadows }) };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function summarizeAudit({ rows, shadows }) {
|
|
316
|
+
const count = (state) => rows.filter((row) => row.state === state).length;
|
|
317
|
+
const summary = { behind: count('BEHIND'), broken: count('BROKEN'),
|
|
318
|
+
unverified: count('INSTALLED_UNVERIFIED') + count('UNRESOLVED'), shadows: shadows.length,
|
|
319
|
+
stale: shadows.filter((s) => s.global && isBehind(s.version, s.global)).length };
|
|
320
|
+
summary.verdict = summary.behind || summary.broken || summary.shadows ? 'DRIFT'
|
|
321
|
+
: summary.unverified || !rows.length ? 'UNKNOWN' : 'PASS';
|
|
322
|
+
summary.exitCode = summary.verdict === 'PASS' ? 0 : summary.verdict === 'UNKNOWN' ? 4 : 1;
|
|
323
|
+
return summary;
|
|
325
324
|
}
|
|
326
325
|
|
|
327
326
|
function report({ rows, shadows, stale }) {
|
|
328
|
-
const w = Math.max(...rows.map((r) => r.name.length));
|
|
327
|
+
const w = Math.max(1, ...rows.map((r) => r.name.length));
|
|
329
328
|
const nPlugin = rows.filter((r) => r.source === 'plugin').length;
|
|
330
329
|
const nNpm = rows.length - nPlugin;
|
|
331
|
-
log(`\n RuvNet stack — ${rows.length}
|
|
330
|
+
log(`\n RuvNet stack — ${rows.length} distribution records (${nNpm} npm-global, ${nPlugin} Claude Code plugin)\n`);
|
|
332
331
|
for (const r of rows) {
|
|
333
|
-
const mark = { CURRENT: ' ok ', BEHIND: 'BEHIND', AHEAD: ' ahead', BROKEN: 'BROKEN', UNRESOLVED: ' ?? ' }[r.state];
|
|
332
|
+
const mark = { CURRENT: ' ok ', BEHIND: 'BEHIND', AHEAD: ' ahead', BROKEN: 'BROKEN', UNRESOLVED: ' ?? ', INSTALLED_UNVERIFIED: 'UNVERIFIED' }[r.state];
|
|
334
333
|
const detail = r.state === 'BEHIND' ? `${r.installed} -> ${r.target} (@${r.tag})`
|
|
335
334
|
: r.state === 'AHEAD' ? `${r.installed} (ahead of @${r.tag} ${r.target} — alpha track; left alone)`
|
|
336
335
|
: r.state === 'BROKEN' ? `no readable version on disk; registry has ${r.target ?? '?'}`
|
|
337
|
-
: r.source === 'plugin' ? `${r.installed} (plugin · ${r.marketplace} marketplace)`
|
|
336
|
+
: r.source === 'plugin' ? `${r.installed} (plugin · ${r.marketplace}; marketplace currency NOT CHECKED; ${r.instances?.length || 1} scope record(s))`
|
|
338
337
|
: r.installed;
|
|
339
338
|
log(` [${mark}] ${r.name.padEnd(w)} ${detail}`);
|
|
339
|
+
if (r.instances?.length > 1) for (const instance of r.instances) log(` ${instance.scope}: ${instance.version || 'UNREADABLE'} (${instance.readable ? 'disk' : 'record only'})`);
|
|
340
340
|
}
|
|
341
341
|
if (shadows.length) {
|
|
342
342
|
log(`\n npx shadow copies:`);
|
|
343
343
|
for (const s of shadows) {
|
|
344
|
-
log(` ${s.name}@${s.version}${s.global && s.version
|
|
344
|
+
log(` ${s.name}@${s.version}${s.global && isBehind(s.version, s.global) ? ` STALE — global is ${s.global}` : ''}`);
|
|
345
345
|
}
|
|
346
346
|
}
|
|
347
347
|
log('');
|
|
@@ -391,8 +391,9 @@ function sync({ dryRun = false } = {}) {
|
|
|
391
391
|
const toInstall = [...behind, ...broken.filter((r) => r.target)].map((r) => `${r.name}@${r.target}`);
|
|
392
392
|
|
|
393
393
|
if (!toInstall.length && !stale.length) {
|
|
394
|
-
log(
|
|
395
|
-
writeReceipt(a, [], []);
|
|
394
|
+
log(` No npm repair selected. Audit: ${JSON.stringify(summarizeAudit(a))}`);
|
|
395
|
+
if (!dryRun) writeReceipt(a, [], []);
|
|
396
|
+
process.exitCode = summarizeAudit(a).exitCode;
|
|
396
397
|
return;
|
|
397
398
|
}
|
|
398
399
|
if (dryRun) {
|
|
@@ -431,7 +432,10 @@ function sync({ dryRun = false } = {}) {
|
|
|
431
432
|
}
|
|
432
433
|
if (wrong.length) die(`npm reported success but THE DISK DISAGREES:\n - ${wrong.join('\n - ')}`);
|
|
433
434
|
|
|
434
|
-
|
|
435
|
+
const after = audit();
|
|
436
|
+
writeReceipt(after, toInstall, purged);
|
|
437
|
+
process.exitCode = summarizeAudit(after).exitCode;
|
|
438
|
+
log(` Remaining audit: ${JSON.stringify(summarizeAudit(after))}`);
|
|
435
439
|
log(`\n synced ${toInstall.length} package(s); purged ${purged.length} shadow(s); verified against disk.`);
|
|
436
440
|
}
|
|
437
441
|
|
|
@@ -440,18 +444,12 @@ if (process.argv[1] && path.resolve(process.argv[1]).endsWith('stack-sync.mjs'))
|
|
|
440
444
|
const args = process.argv.slice(2);
|
|
441
445
|
if (args.includes('--audit')) {
|
|
442
446
|
const a = audit();
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
log(` DRIFT: ${behind.length} behind, ${broken.length} broken, ${stale.length} stale shadow(s)` +
|
|
450
|
-
(unres ? `, ${unres} UNMEASURED (registry unreachable)` : '') + '.');
|
|
451
|
-
log(` Fix: node scripts/stack-sync.mjs --sync\n`);
|
|
452
|
-
process.exit(1); // non-zero: a watchdog must never call a drifted stack "green"
|
|
453
|
-
}
|
|
454
|
-
log(' one copy of everything, all current, no shadows.\n');
|
|
447
|
+
report(a);
|
|
448
|
+
const summary = summarizeAudit(a);
|
|
449
|
+
log(` Audit: ${JSON.stringify(summary)}`);
|
|
450
|
+
if (summary.unverified) log(' Required: verify each plugin against its marketplace revision and each unresolved npm target against registry tags.');
|
|
451
|
+
if (summary.shadows) log(' Cached copies are present; inspect invocation paths before removing any cache. No copies removed by audit.');
|
|
452
|
+
process.exitCode = summary.exitCode;
|
|
455
453
|
} else if (args.includes('--sync')) {
|
|
456
454
|
sync({ dryRun: args.includes('--dry-run') });
|
|
457
455
|
} else {
|
|
@@ -4,8 +4,48 @@ import fs from 'node:fs';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { runHostMatrixAsync } from './host-install-matrix.mjs';
|
|
7
|
+
import { validatePlanAgainstCoverage } from './retrieval-canary.mjs';
|
|
8
|
+
import { extractZip } from '../kb/zip-extract.mjs';
|
|
9
|
+
import { tarExtractionInvocation } from './publication-receipt.mjs';
|
|
7
10
|
|
|
8
11
|
const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
12
|
+
|
|
13
|
+
export function readCandidateRetrieval({ manifest, planFile, coverageFile }) {
|
|
14
|
+
if (!planFile || !coverageFile) throw new Error('candidate retrieval requires sealed plan and coverage files');
|
|
15
|
+
const plan = JSON.parse(fs.readFileSync(planFile));
|
|
16
|
+
const coverageBytes = fs.readFileSync(coverageFile);
|
|
17
|
+
const coverage = JSON.parse(coverageBytes);
|
|
18
|
+
// An observed baseline defines the delta only; it never supplies candidate PASS evidence.
|
|
19
|
+
validatePlanAgainstCoverage(plan, coverage, { allowObservedBaseline: true });
|
|
20
|
+
const npm = manifest.members?.filter(({ role }) => role === 'npm') || [];
|
|
21
|
+
const bundle = manifest.members?.filter(({ role }) => role === 'bundle') || [];
|
|
22
|
+
const coverageSha256 = crypto.createHash('sha256').update(coverageBytes).digest('hex');
|
|
23
|
+
if (npm.length !== 1 || bundle.length !== 1
|
|
24
|
+
|| plan.candidate.sourceSha !== manifest.candidateSha
|
|
25
|
+
|| plan.candidate.packageSha256 !== npm[0].sha256 || plan.candidate.archiveSha256 !== bundle[0].sha256
|
|
26
|
+
|| plan.coverage.sha256 !== coverageSha256 || plan.candidate.coverageSha256 !== coverageSha256
|
|
27
|
+
|| plan.coverage.bytes !== coverageBytes.length
|
|
28
|
+
|| coverage.releaseIdentity?.sourceSnapshot !== manifest.candidateSha
|
|
29
|
+
|| coverage.releaseIdentity?.version !== manifest.version || coverage.releaseIdentity?.tag !== manifest.tag
|
|
30
|
+
|| plan.candidate.publicLedgerSha256 !== coverage.generationLedger?.sha256
|
|
31
|
+
|| plan.candidate.publicLedgerBytes !== coverage.generationLedger?.bytes
|
|
32
|
+
|| plan.candidate.publicStoreCount !== coverage.generationLedger?.storeCount
|
|
33
|
+
|| plan.candidate.publicInventoryPartitionSha256 !== coverage.publicInventoryPartitionSha256) {
|
|
34
|
+
throw new Error('candidate retrieval plan, coverage or artifact identity mismatch');
|
|
35
|
+
}
|
|
36
|
+
return { plan, sourceSha: manifest.candidateSha, artifactSha256: npm[0].sha256,
|
|
37
|
+
candidateArchiveSha256: bundle[0].sha256, packageBytes: npm[0].size, bundleBytes: bundle[0].size };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function verifyCandidateRetrievalAssets({ retrieval, assets }) {
|
|
41
|
+
for (const [file, digest, bytes] of [[assets.packagePath, retrieval.artifactSha256, retrieval.packageBytes],
|
|
42
|
+
[assets.bundlePath, retrieval.candidateArchiveSha256, retrieval.bundleBytes]]) {
|
|
43
|
+
if (!Number.isSafeInteger(bytes) || bytes < 1 || fs.statSync(file).size !== bytes || sha256(file) !== digest) {
|
|
44
|
+
throw new Error(`candidate artifact does not match sealed retrieval input: ${file}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
9
49
|
const locate = (name) => {
|
|
10
50
|
try { return execFileSync('which', [name], { encoding: 'utf8' }).trim(); } catch { return null; }
|
|
11
51
|
};
|
|
@@ -23,25 +63,32 @@ const run = (name, args, options) => {
|
|
|
23
63
|
// tests/unit/staged-host-verifier.test.mjs pins it, but it is now an alias, not a second copy.
|
|
24
64
|
export { classifyDoctor as classifyDoctorResult } from './host-install-matrix.mjs';
|
|
25
65
|
|
|
26
|
-
const preparePackage = ({ packagePath, bundlePath }) => {
|
|
66
|
+
const preparePackage = async ({ packagePath, bundlePath }) => {
|
|
27
67
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-staged-host-'));
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
fs.
|
|
68
|
+
try {
|
|
69
|
+
const extraction = tarExtractionInvocation(packagePath, temp);
|
|
70
|
+
run('tar', extraction.args, { cwd: extraction.cwd });
|
|
71
|
+
const packageRoot = path.join(temp, 'package');
|
|
72
|
+
const bundleRoot = path.join(packageRoot, 'dist', 'ruvnet-brain');
|
|
73
|
+
fs.mkdirSync(bundleRoot, { recursive: true });
|
|
74
|
+
await extractZip(bundlePath, bundleRoot);
|
|
75
|
+
const nested = path.join(bundleRoot, 'ruvnet-brain');
|
|
76
|
+
if (fs.existsSync(nested)) {
|
|
77
|
+
for (const name of fs.readdirSync(nested)) fs.renameSync(path.join(nested, name), path.join(bundleRoot, name));
|
|
78
|
+
fs.rmdirSync(nested);
|
|
79
|
+
}
|
|
80
|
+
return { temp, packageRoot };
|
|
81
|
+
} catch (error) {
|
|
82
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
83
|
+
throw error;
|
|
37
84
|
}
|
|
38
|
-
return { temp, packageRoot };
|
|
39
85
|
};
|
|
40
86
|
|
|
41
|
-
export function stagedHostVerifier({ assets, identity }) {
|
|
87
|
+
export function stagedHostVerifier({ assets, identity, retrieval }, { runMatrix = runHostMatrixAsync } = {}) {
|
|
42
88
|
return {
|
|
43
89
|
async verify({ source, assets: observedAssets = assets }) {
|
|
44
|
-
|
|
90
|
+
if (retrieval) verifyCandidateRetrievalAssets({ retrieval, assets: observedAssets });
|
|
91
|
+
const prepared = await preparePackage(observedAssets);
|
|
45
92
|
try {
|
|
46
93
|
// The loop, the mode names, the env and the doctor verdict all live in
|
|
47
94
|
// scripts/host-install-matrix.mjs, shared with the published-side check in
|
|
@@ -50,13 +97,15 @@ export function stagedHostVerifier({ assets, identity }) {
|
|
|
50
97
|
// install shape than the one that runs after publication — so the two halves of a release
|
|
51
98
|
// were judging different things and could not be compared. Only the STAGED-vs-PUBLISHED
|
|
52
99
|
// difference is real, and it is now a named variant rather than a second implementation.
|
|
53
|
-
const matrix = await
|
|
100
|
+
const matrix = await runMatrix({
|
|
54
101
|
packageRoot: prepared.packageRoot,
|
|
55
102
|
version: identity.version,
|
|
56
103
|
variant: 'staged',
|
|
57
104
|
locate,
|
|
58
105
|
temp: prepared.temp,
|
|
106
|
+
retrieval,
|
|
59
107
|
});
|
|
108
|
+
if (retrieval) verifyCandidateRetrievalAssets({ retrieval, assets: observedAssets });
|
|
60
109
|
if (matrix.verdict !== 'PASS') {
|
|
61
110
|
return { verdict: 'FAIL', source, error: matrix.error, fixtures: matrix.fixtures };
|
|
62
111
|
}
|
package/scripts/wired-check.mjs
CHANGED
|
@@ -69,6 +69,8 @@ const argv = process.argv.slice(2);
|
|
|
69
69
|
* every entry on every run, below, so they cannot rot unseen.
|
|
70
70
|
*/
|
|
71
71
|
const STANDALONE = [
|
|
72
|
+
['gate', 'retired automatic-hook helper retained for explicit use and tests'],
|
|
73
|
+
['version-bump-gate', 'retired automatic interceptor; explicit version checks own release validation'],
|
|
72
74
|
['lesson-seed', 'one-shot seeding, run deliberately by a human'],
|
|
73
75
|
['lesson-ratify', 'the human control surface — a CLI is its entire purpose'],
|
|
74
76
|
['stamp-sweep', 'ADR-056 §2 — the ONE-TIME backfill half of the stamp rule. A human runs it once '
|
|
@@ -95,6 +97,12 @@ const STANDALONE = [
|
|
|
95
97
|
+ 'RVFs to RVF-GENERATIONS.json and optionally prunes legacy sidecars; build-bundle.mjs consumes '
|
|
96
98
|
+ 'and validates the resulting manifest, so scheduling this destructive migration would be wrong'],
|
|
97
99
|
['release', 'the ship path, run by a human'],
|
|
100
|
+
['corpus-seed-publish', 'privileged manual corpus publication wrapper; corpus-seed.yml explicitly '
|
|
101
|
+
+ 'stops at sealed preparation with contents:read. This entry validates receipt/archive inputs '
|
|
102
|
+
+ 'and delegates mutation to release.mjs protected authority. No operational caller is registered; '
|
|
103
|
+
+ 'classification does not claim publication works or has occurred'],
|
|
104
|
+
['gate', 'manual benchmark harness: rebuilds concepts and runs three routing proof batteries; '
|
|
105
|
+
+ 'no workflow or scheduler invokes this expensive command'],
|
|
98
106
|
['execution-preflight', 'external orchestration boundary — invoked by the host before consequential Ruflo/Codex execution; no in-repo caller exists because the host supplies the live Brain and AgentDB receipts'],
|
|
99
107
|
['fix-workstream', 'session-supervised coordination CLI run explicitly by the integration owner or an '
|
|
100
108
|
+ 'isolated writing agent to start and hand off a fix lane. Scheduling it would violate its safety '
|
|
@@ -375,8 +383,11 @@ function callerFiles(repo = REPO) {
|
|
|
375
383
|
*/
|
|
376
384
|
export function callerPattern(fileName) {
|
|
377
385
|
const q = fileName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
386
|
+
// Filenames must be complete tokens: gates.mjs is not corpus-aggregates.mjs,
|
|
387
|
+
// and version.mjs.map is not a caller of version.mjs.
|
|
388
|
+
const bounded = `(?<![\\w.-])${q}(?![\\w.-])`;
|
|
378
389
|
// eslint-disable-next-line no-useless-escape
|
|
379
|
-
return new RegExp(`["'\`][^"'\`\\n]*${
|
|
390
|
+
return new RegExp(`["'\`][^"'\`\\n]*${bounded}|(?:node|bash|sh|exec|spawn\\w*)\\s+[^\\n]*${bounded}`);
|
|
380
391
|
}
|
|
381
392
|
|
|
382
393
|
/**
|
|
@@ -765,6 +776,16 @@ export function hookWiringAudit({
|
|
|
765
776
|
homeSettingsFile = path.join(os.homedir(), '.claude', 'settings.json'),
|
|
766
777
|
held = HOOK_HELD,
|
|
767
778
|
} = {}) {
|
|
779
|
+
const retirementFiles = [
|
|
780
|
+
path.join(repo, 'plugin/hooks/hooks.json'),
|
|
781
|
+
path.join(repo, 'plugin/hooks/codex-hooks.json'),
|
|
782
|
+
path.join(repo, '.claude/settings.json'),
|
|
783
|
+
path.join(repo, '.codex/hooks.json'),
|
|
784
|
+
];
|
|
785
|
+
const automaticHooksRetired = retirementFiles.every((file) => {
|
|
786
|
+
const doc = readJsonSafe(file);
|
|
787
|
+
return doc && doc.hooks && commandStrings(doc.hooks).length === 0;
|
|
788
|
+
});
|
|
768
789
|
const table = hookShimTable(repo);
|
|
769
790
|
const codexWrapper = installedCodexHookWrapper(repo);
|
|
770
791
|
const reached = new Map(); // basename -> Set(reason)
|
|
@@ -818,6 +839,11 @@ export function hookWiringAudit({
|
|
|
818
839
|
const isReached = reached.has(f);
|
|
819
840
|
if (!declared && !isReached) continue; // not hook-intended at all — outside the census
|
|
820
841
|
if (isReached) rows.push({ file: f, state: 'wired', sources: [...reached.get(f)] });
|
|
842
|
+
else if (automaticHooksRetired && declared) rows.push({
|
|
843
|
+
file: f,
|
|
844
|
+
state: 'retired',
|
|
845
|
+
why: 'automatic Brain hook registries are intentionally empty; body retained for audit or explicit-command reuse',
|
|
846
|
+
});
|
|
821
847
|
else if (held[f]) rows.push({ file: f, state: 'held', why: held[f] });
|
|
822
848
|
else rows.push({ file: f, state: 'unwired' });
|
|
823
849
|
}
|
|
@@ -872,6 +898,7 @@ if (invokedDirectly) {
|
|
|
872
898
|
const hookAudit = hookWiringAudit();
|
|
873
899
|
const hookBy = (s) => hookAudit.rows.filter((r) => r.state === s);
|
|
874
900
|
const hookUnwired = hookBy('unwired');
|
|
901
|
+
const hookRetired = hookBy('retired');
|
|
875
902
|
|
|
876
903
|
const lessonAudit = lessonTriggerAudit();
|
|
877
904
|
|
|
@@ -915,7 +942,7 @@ if (invokedDirectly) {
|
|
|
915
942
|
console.log(`\n ── HOOK WIRING — plugin/scripts/*.sh|*.mjs vs plugin/hooks/{hooks,codex-hooks}.json, `
|
|
916
943
|
+ `.claude/settings.json, ~/.claude/settings.json ──\n`);
|
|
917
944
|
console.log(` ${hookAudit.rows.length} hook-intended script(s) in the census`);
|
|
918
|
-
console.log(` ${hookBy('wired').length} wired · ${hookBy('held').length} held · `
|
|
945
|
+
console.log(` ${hookBy('wired').length} wired · ${hookRetired.length} retired · ${hookBy('held').length} held · `
|
|
919
946
|
+ `${hookUnwired.length} UNWIRED\n`);
|
|
920
947
|
|
|
921
948
|
for (const u of hookUnwired) {
|
|
@@ -928,6 +955,8 @@ if (invokedDirectly) {
|
|
|
928
955
|
|
|
929
956
|
for (const h of hookBy('held')) console.log(` ⏸ plugin/scripts/${h.file}\n ${h.why}\n`);
|
|
930
957
|
|
|
958
|
+
for (const r of hookRetired) console.log(` ○ plugin/scripts/${r.file}\n ${r.why}\n`);
|
|
959
|
+
|
|
931
960
|
for (const w of hookBy('wired')) {
|
|
932
961
|
console.log(` ✓ plugin/scripts/${w.file}\n via ${w.sources.join('; ')}`);
|
|
933
962
|
}
|