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.
Files changed (87) hide show
  1. package/README.md +26 -9
  2. package/bin/install.mjs +646 -366
  3. package/bin/nightly-refresh.mjs +115 -0
  4. package/kb/brain-profile.mjs +118 -32
  5. package/kb/lifecycle-evidence-retention.mjs +237 -0
  6. package/kb/refresh-run.mjs +367 -0
  7. package/kb/retrieval-result.mjs +39 -0
  8. package/kb/update-storage-transaction.mjs +439 -0
  9. package/package.json +12 -3
  10. package/plugin/.claude-plugin/plugin.json +1 -1
  11. package/plugin/.codex-plugin/plugin.json +1 -1
  12. package/plugin/hooks/codex-hooks.json +2 -165
  13. package/plugin/hooks/hook-contracts.json +4 -65
  14. package/plugin/hooks/hooks.json +2 -208
  15. package/plugin/scripts/adr-currency-gate.mjs +29 -19
  16. package/plugin/scripts/capability-registry.mjs +10 -83
  17. package/plugin/scripts/codex-hook-adapter.mjs +62 -9
  18. package/plugin/scripts/codex-hook-wrapper.mjs +19 -1
  19. package/plugin/scripts/continuation-gate.mjs +30 -28
  20. package/plugin/scripts/continuation-objective.mjs +34 -0
  21. package/plugin/scripts/development-maintenance.mjs +49 -0
  22. package/plugin/scripts/hook-shim.mjs +3 -0
  23. package/plugin/scripts/learn-flush.mjs +6 -1
  24. package/plugin/scripts/lesson-gate.mjs +5 -2
  25. package/plugin/scripts/lesson-presentation.mjs +5 -0
  26. package/plugin/scripts/md-stamp.mjs +109 -6
  27. package/plugin/scripts/memory-doctor.mjs +3 -7
  28. package/plugin/scripts/nightly-controller.mjs +9 -27
  29. package/plugin/scripts/nightly-scheduler.mjs +494 -0
  30. package/plugin/scripts/project-progression-contract.mjs +4 -4
  31. package/plugin/scripts/project-progression-store.mjs +35 -6
  32. package/plugin/scripts/ruflo-bin.mjs +20 -0
  33. package/plugin/scripts/session-snapshot-contract.mjs +6 -1
  34. package/plugin/scripts/version-bump-gate.sh +14 -2
  35. package/plugin/skills/release-proof/SKILL.md +6 -2
  36. package/plugin/skills/release-proof/references/receipt-contract.md +31 -2
  37. package/plugin/skills/release-proof/scripts/release-proof.mjs +11 -2
  38. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +15 -23
  39. package/scripts/build-bundle.mjs +8 -0
  40. package/scripts/build-primer.mjs +2 -3
  41. package/scripts/candidate-host-evidence.mjs +59 -41
  42. package/scripts/ci/build-fixture-kb.mjs +59 -2
  43. package/scripts/ci/mutate-hook-timeout.mjs +24 -47
  44. package/scripts/ci/stranger-scenario.mjs +13 -28
  45. package/scripts/claims-verify.mjs +219 -21
  46. package/scripts/console-engine.mjs +4 -18
  47. package/scripts/console-runtime-identity.mjs +6 -0
  48. package/scripts/development-maintenance.mjs +47 -0
  49. package/scripts/development-push-check.mjs +35 -0
  50. package/scripts/distill-project.mjs +24 -47
  51. package/scripts/doc-currency.mjs +91 -19
  52. package/scripts/git-hooks/pre-push +4 -162
  53. package/scripts/health-repair.mjs +22 -4
  54. package/scripts/hook-retirement-check.mjs +22 -0
  55. package/scripts/host-install-matrix.mjs +125 -61
  56. package/scripts/integration-evidence.mjs +11 -6
  57. package/scripts/learning-replay-fixture.mjs +23 -2
  58. package/scripts/nightly-package-observation.mjs +67 -0
  59. package/scripts/nightly-two-run-proof.mjs +489 -0
  60. package/scripts/nightly-watchdog.mjs +17 -2
  61. package/scripts/npm-invocation.mjs +20 -0
  62. package/scripts/prepublication-evidence.mjs +42 -6
  63. package/scripts/primer-grounding.mjs +20 -0
  64. package/scripts/product-integrity-contract.mjs +22 -2
  65. package/scripts/public-verification-abandon.mjs +101 -0
  66. package/scripts/public-verification-aggregate.mjs +61 -19
  67. package/scripts/public-verification-finalizer.mjs +8 -2
  68. package/scripts/public-verification-lane.mjs +64 -8
  69. package/scripts/publication-receipt.mjs +156 -50
  70. package/scripts/published-surface-probe.mjs +10 -2
  71. package/scripts/qa-contract.mjs +57 -0
  72. package/scripts/qa-lanes.mjs +40 -0
  73. package/scripts/qa-runner.mjs +54 -64
  74. package/scripts/qe/ux-suite.mjs +10 -34
  75. package/scripts/qualified-candidate-check.mjs +124 -0
  76. package/scripts/release-abort-stale.mjs +2 -2
  77. package/scripts/release-projection.mjs +20 -1
  78. package/scripts/release-qualification-contract.mjs +87 -0
  79. package/scripts/release-qualification.mjs +135 -0
  80. package/scripts/release-transaction.mjs +81 -1
  81. package/scripts/remedy-registry.mjs +6 -13
  82. package/scripts/retrieval-canary.mjs +91 -18
  83. package/scripts/selfcheck.mjs +12 -4
  84. package/scripts/snapshot-freshness.mjs +58 -0
  85. package/scripts/stack-sync.mjs +41 -43
  86. package/scripts/staged-host-verifier.mjs +63 -14
  87. package/scripts/wired-check.mjs +31 -2
@@ -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
- // Merge in plugin-sourced tools (ISSUE #22). Dedup by name: a tool present BOTH globally and as a
213
- // plugin appears once, and the global-npm copy wins — it is the one classify() can compare against
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
- // ISSUE #22 — a Claude Code plugin tracks ITS MARKETPLACE's update cadence, not npm semver. There
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: p.installed, state: p.installed ? 'CURRENT' : 'BROKEN' };
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
- // Guard scoped to npm rows (ISSUE #22): plugin rows are CURRENT by construction, so counting them
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 !== s.global) };
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 !== s.global);
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} packages (${nNpm} npm-global, ${nPlugin} Claude Code plugin)\n`);
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 !== s.global ? ` STALE — global is ${s.global}` : ''}`);
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(' one copy of everything, all current, no shadows. Nothing to do.');
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
- writeReceipt(audit(), toInstall, purged);
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
- const { behind, broken, stale } = report(a);
444
- // UNRESOLVED counts as drift, not as health: a package we could not measure is a package we
445
- // cannot vouch for. Reporting "current" for something we never checked is the lie this tool exists to stop.
446
- const unres = a.unresolved.length;
447
- const bad = behind.length + broken.length + stale.length + unres;
448
- if (bad) {
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
- run('tar', ['-xzf', packagePath, '-C', temp]);
29
- const packageRoot = path.join(temp, 'package');
30
- const bundleRoot = path.join(packageRoot, 'dist', 'ruvnet-brain');
31
- fs.mkdirSync(bundleRoot, { recursive: true });
32
- run('unzip', ['-q', bundlePath, '-d', bundleRoot]);
33
- const nested = path.join(bundleRoot, 'ruvnet-brain');
34
- if (fs.existsSync(nested)) {
35
- for (const name of fs.readdirSync(nested)) fs.renameSync(path.join(nested, name), path.join(bundleRoot, name));
36
- fs.rmdirSync(nested);
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
- const prepared = preparePackage(observedAssets);
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 runHostMatrixAsync({
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
  }
@@ -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]*${q}|(?:node|bash|sh|exec|spawn\\w*)\\s+[^\\n]*${q}`);
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
  }