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
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  // plugin/scripts/md-stamp.mjs — PostToolUse (Write|Edit|MultiEdit). Refreshes an EXISTING doc
3
3
  // stamp's date to today, in-place, whenever a touched .md file's stamp has gone stale.
4
+ // RUVNET_MD_STAMP=ensure explicitly enables managed creation/update timestamps and content-digest
5
+ // idempotence. That mode uses observed file edit times in UTC; unknown creation remains unknown.
4
6
  //
5
7
  // WHY. The owner: "I told you to update all .md docs with time/date stamps when touched. I don't
6
8
  // want to have to remind you again in any repo." Today that convention lives only in the model's
@@ -31,7 +33,9 @@
31
33
  import fs from 'node:fs';
32
34
  import path from 'node:path';
33
35
  import { fileURLToPath } from 'node:url';
36
+ import { createHash } from 'node:crypto';
34
37
  import { parseHookEvent, toolName, field, readStdinBounded } from './hook-input.mjs';
38
+ import { contains, projectDirectory } from './project-identity.mjs';
35
39
 
36
40
  // ── date, from the system clock, formatted in the repo's standard timezone ─────────────────────────
37
41
  // Same idiom as scripts/self-update.mjs's README badge stamp: Intl.DateTimeFormat is a Node builtin
@@ -173,9 +177,90 @@ export function ensureStamp(content, { updated, created } = {}) {
173
177
 
174
178
  /** Pure: given a .md file's current bytes, return the bytes it should have. Identical in ⇒ identical out. */
175
179
  export function computeStampedContent(content, today = todayNY()) {
180
+ if (isPinned(content)) return content;
176
181
  return refreshPlainStamp(refreshFrontmatterStamp(content, today), today);
177
182
  }
178
183
 
184
+ function isPinned(content) {
185
+ return /^updated_pinned:[ \t]*(?:true|'true'|"true")[ \t]*(?:#.*)?$/m.test(content.match(FRONTMATTER_BLOCK_RE)?.[0] || '');
186
+ }
187
+
188
+ // Explicit RUVNET_MD_STAMP=ensure opts this project into managed metadata. Defaults remain
189
+ // refresh-only: installing the plugin is not consent to impose stamps on a stranger's documents.
190
+ // The digest covers authored bytes, excluding only metadata we maintain. Unlike restoring mtime
191
+ // after writing, it remains idempotent when filesystem timestamp operations fail or hooks repeat.
192
+ const MANAGED_EXCLUDED = new Set(['node_modules', '.git', 'kb', 'dist', 'clones', 'archive',
193
+ '.agentic-qe', 'coverage', '.next', 'build', 'tmp', '.swarm', 'vendor']);
194
+ const DIGEST_LINE = /^<!-- ruvnet-md-stamp: ([a-f0-9]{64}) -->\r?\n?/m;
195
+ const STAMP_VALUE = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}\.\d{3}Z)?$/;
196
+ const MANAGED_HEAD_LINES = PLAIN_STAMP_MAX_LINES + 6;
197
+
198
+ function stampBody(content) {
199
+ const fm = content.match(FRONTMATTER_BLOCK_RE)?.[0];
200
+ if (fm) return fm.replace(/^(?:updated|updated_at|created_at|date|stamp_content_sha256):[^\n]*\n/gm, '') + content.slice(fm.length);
201
+ const head = headSlice(content, MANAGED_HEAD_LINES);
202
+ return head.replace(/^([ \t]*`?(?:Updated|Created):)[^\n]*\n?/gm, '')
203
+ .replace(DIGEST_LINE, '') + content.slice(head.length);
204
+ }
205
+
206
+ /** Managed mode: edit time is observed by the caller; creation requires positive provenance. */
207
+ export function computeManagedStamp(content, { updated, created = null } = {}) {
208
+ if (!STAMP_VALUE.test(updated || '') || isPinned(content)) return content;
209
+ const at = stampInsertionPoint(content);
210
+ if (!at) return content;
211
+ const fm = content.match(FRONTMATTER_BLOCK_RE)?.[0];
212
+ const oldDigest = fm?.match(/^stamp_content_sha256: ([a-f0-9]{64})$/m)?.[1]
213
+ || headSlice(content, MANAGED_HEAD_LINES).match(DIGEST_LINE)?.[1];
214
+ const digest = (text) => createHash('sha256').update(stampBody(text)).digest('hex');
215
+ const complete = fm
216
+ ? ['date', 'updated', 'created_at', 'updated_at'].every((key) => new RegExp(`^${key}: .+`, 'm').test(fm))
217
+ : ['Created', 'Updated'].every((key) => new RegExp(`^[ \\t]*\u0060?${key}: .+`, 'm').test(headSlice(content, MANAGED_HEAD_LINES)));
218
+ if (complete && oldDigest && oldDigest === digest(content)) return content;
219
+ const creation = STAMP_VALUE.test(created || '') ? created : null;
220
+ let out;
221
+ if (fm) {
222
+ let block = fm;
223
+ const put = (key, value, preserve = false) => {
224
+ const re = new RegExp(`^${key}:[^\\n]*`, 'm');
225
+ if (re.test(block)) { if (!preserve) block = block.replace(re, `${key}: ${value}`); }
226
+ else { const closing = block.lastIndexOf('---'); block = block.slice(0, closing) + `${key}: ${value}\n` + block.slice(closing); }
227
+ };
228
+ put('date', creation?.slice(0, 10) || 'unknown', true);
229
+ put('created_at', creation || 'unknown', true);
230
+ put('updated', updated.slice(0, 10));
231
+ put('updated_at', updated);
232
+ put('stamp_content_sha256', 'pending');
233
+ out = block + content.slice(fm.length);
234
+ // Hash after insertion so placement whitespace cannot make the next delivery appear edited.
235
+ out = out.replace(/^stamp_content_sha256: pending$/m, `stamp_content_sha256: ${digest(out)}`);
236
+ } else {
237
+ let head = headSlice(content, MANAGED_HEAD_LINES);
238
+ const rest = content.slice(head.length);
239
+ if (!head.endsWith('\n')) head += '\n';
240
+ const updatedRe = /^([ \t]*`?Updated:[ \t]*)[^\n|`]*(.*)$/m;
241
+ if (updatedRe.test(head)) head = head.replace(updatedRe, (_match, prefix, suffix) => `${prefix}${updated}${suffix ? ` ${suffix.trimStart()}` : ''}`);
242
+ else head = head.slice(0, at.index) + `\nUpdated: ${updated}\n` + head.slice(at.index);
243
+ if (!/^[ \t]*`?Created:/m.test(head)) head = head.replace(/^(.*Updated:[^\n]*\n)/m, `$1Created: ${creation || 'unknown (not recorded)'}\n`);
244
+ if (DIGEST_LINE.test(head)) head = head.replace(DIGEST_LINE, '<!-- ruvnet-md-stamp: pending -->\n');
245
+ else head = head.replace(/^(.*Created:[^\n]*\n)/m, '$1<!-- ruvnet-md-stamp: pending -->\n');
246
+ out = head + rest;
247
+ const withoutPending = out.replace(/^<!-- ruvnet-md-stamp: pending -->\n/m, '');
248
+ out = out.replace('<!-- ruvnet-md-stamp: pending -->', `<!-- ruvnet-md-stamp: ${digest(withoutPending)} -->`);
249
+ }
250
+ return out;
251
+ }
252
+
253
+ /** Avoid overwriting a file changed since the hook's read. Session leases still govern writers. */
254
+ export function writeStampIfUnchanged(file, original, stamped, observed) {
255
+ if (stamped === original) return false;
256
+ const current = fs.statSync(file);
257
+ if (current.dev !== observed.dev || current.ino !== observed.ino
258
+ || current.mtimeMs !== observed.mtimeMs || current.ctimeMs !== observed.ctimeMs
259
+ || fs.readFileSync(file, 'utf8') !== original) return false;
260
+ fs.writeFileSync(file, stamped);
261
+ return true;
262
+ }
263
+
179
264
  // ── the hook body ────────────────────────────────────────────────────────────────────────────────
180
265
  async function readHookInput() {
181
266
  // Never block waiting on stdin: a TTY (someone running this file by hand) has none to give.
@@ -193,19 +278,37 @@ async function main() {
193
278
 
194
279
  const ev = await readHookInput();
195
280
  if (!['Write', 'Edit', 'MultiEdit'].includes(toolName(ev))) return; // wrong tool: do nothing
281
+ if (ev?.hook_event_name && ev.hook_event_name !== 'PostToolUse') return;
282
+ if (ev?.tool_response?.isError || ev?.tool_response?.error || ev?.tool_response?.success === false) return;
196
283
 
197
- const filePath = field(ev, 'tool_input.file_path');
198
- if (!filePath || path.extname(filePath).toLowerCase() !== '.md') return; // wrong file: do nothing
284
+ const supplied = field(ev, 'tool_input.file_path');
285
+ if (!supplied || path.extname(supplied).toLowerCase() !== '.md') return;
286
+ const root = projectDirectory();
287
+ const filePath = path.resolve(root, supplied);
288
+ if (!contains(root, filePath)) return;
289
+ if (path.relative(root, fs.realpathSync(filePath)).split(path.sep).some((part) => MANAGED_EXCLUDED.has(part))) return;
290
+ // The shared helper is present in current generations. Older standalone payloads can omit it.
291
+ try {
292
+ const { developmentHooksSuspended } = await import('./development-maintenance.mjs');
293
+ if (developmentHooksSuspended(root)) return;
294
+ } catch (error) { if (error.code !== 'ERR_MODULE_NOT_FOUND') return; }
199
295
 
200
- let original;
201
- try { original = fs.readFileSync(filePath, 'utf8'); } catch { return; } // unreadable/gone: exit 0
296
+ let original, observed;
297
+ try { observed = fs.statSync(filePath); original = fs.readFileSync(filePath, 'utf8'); } catch { return; }
202
298
 
203
299
  let stamped;
204
- try { stamped = computeStampedContent(original); } catch { return; } // malformed content: exit 0
300
+ try {
301
+ stamped = sw === 'ensure'
302
+ ? computeManagedStamp(original, {
303
+ updated: observed.mtime.toISOString(),
304
+ created: ev?.tool_response?.type === 'create' ? observed.mtime.toISOString() : null,
305
+ })
306
+ : computeStampedContent(original);
307
+ } catch { return; }
205
308
 
206
309
  if (stamped === original) return; // already current (or no stamp at all) — NEVER write; loop guard
207
310
 
208
- try { fs.writeFileSync(filePath, stamped); } catch { /* advisory — a failed write is not our problem */ }
311
+ try { writeStampIfUnchanged(filePath, original, stamped, observed); } catch { /* advisory */ }
209
312
  }
210
313
 
211
314
  function isMain() {
@@ -111,14 +111,10 @@ const q = (db, sql) => {
111
111
 
112
112
  try {
113
113
  const value = run(uri);
114
- // Belt and suspenders for a third SQLite build we have not measured: a read that started with
115
- // no sidecars must end with no sidecars. If one appeared anyway — a fresh concurrent writer, or
116
- // another version-specific quirk — the bytes just read cannot be trusted, and this read's own
117
- // litter is removed rather than left in the caller's directory.
114
+ // A concurrent writer invalidates the immutable-read assumption. Reject this result, but
115
+ // never remove sidecars: they may contain that writer's committed data and we do not own them.
116
+ // A later diagnosis will select ordinary read-only mode while the sidecars remain present.
118
117
  if (restingWal && walSidecarsPresent(db)) {
119
- for (const suffix of ['-wal', '-shm']) {
120
- try { fs.unlinkSync(`${db}${suffix}`); } catch { /* best-effort cleanup only */ }
121
- }
122
118
  return { ok: false, err: 'a writer opened the store mid-read' };
123
119
  }
124
120
  return { ok: true, value, viaImmutable: restingWal || undefined };
@@ -4,11 +4,12 @@
4
4
  // console reach the same `bin/install.mjs --enable-nightly/--disable-nightly` door; this adapter only
5
5
  // supplies structured status and captures its exit result for the console.
6
6
 
7
- import fs from 'node:fs';
8
- import os from 'node:os';
9
7
  import path from 'node:path';
8
+ import os from 'node:os';
10
9
  import { spawnSync } from 'node:child_process';
11
10
  import { fileURLToPath } from 'node:url';
11
+ import { NIGHTLY_LABEL, nightlyArtifact, schedulerStatus } from './nightly-scheduler.mjs';
12
+ export { NIGHTLY_LABEL, nightlyArtifact } from './nightly-scheduler.mjs';
12
13
 
13
14
  // ROOT is the tree that holds `bin/install.mjs`, and it is resolved by an EXACT layout test rather
14
15
  // than by `..` — because `..` means two different things since this file moved into the payload
@@ -44,8 +45,6 @@ const INSTALLER = path.join(ROOT, 'bin', 'install.mjs');
44
45
  * tests/unit/nightly-job-identity.test.mjs asserts the two are the same string, so a drift is a red
45
46
  * test rather than a capability that quietly disappears from the console.
46
47
  */
47
- export const NIGHTLY_LABEL = 'com.ruvnet.brain-update';
48
-
49
48
  function schedulerEnvironment(env) {
50
49
  const fixtureRoot = env.RUVNET_CONSOLE_ROOT;
51
50
  if (!fixtureRoot) return env;
@@ -61,29 +60,12 @@ function schedulerEnvironment(env) {
61
60
  }
62
61
  return { ...env, HOME: isolatedHome, USERPROFILE: isolatedHome };
63
62
  }
64
-
65
- export function nightlyArtifact({ env = process.env, platform = process.platform } = {}) {
66
- const schedulerEnv = schedulerEnvironment(env);
67
- const home = schedulerEnv.HOME || os.homedir();
68
- return {
69
- supported: platform === 'darwin',
70
- platform,
71
- path: path.join(home, 'Library', 'LaunchAgents', `${NIGHTLY_LABEL}.plist`),
72
- label: NIGHTLY_LABEL,
73
- };
74
- }
75
-
76
63
  export function nightlyStatus(options = {}) {
77
- const artifact = nightlyArtifact(options);
78
- if (!artifact.supported) {
79
- return { state: 'unsupported', evidence: `No reversible scheduler adapter is implemented for ${artifact.platform}.`, artifact };
80
- }
81
- const present = fs.existsSync(artifact.path);
82
- return {
83
- state: present ? 'on' : 'off',
84
- evidence: present ? `LaunchAgent plist exists at ${artifact.path}` : `No LaunchAgent plist at ${artifact.path}`,
85
- artifact,
86
- };
64
+ const env = schedulerEnvironment(options.env || process.env);
65
+ const brainHome = options.brainHome || env.RUVNET_BRAIN_HOME
66
+ || path.join(env.HOME || env.USERPROFILE || '', '.cache', 'ruvnet-brain');
67
+ return schedulerStatus({ ...options, env, brainHome, kbDir: options.kbDir || env.RUVNET_BRAIN_KB,
68
+ testMode: options.testMode ?? env.RUVNET_BRAIN_SCHEDULER_TEST === '1' });
87
69
  }
88
70
 
89
71
  export function applyNightlyChoice(enabled, options = {}) {
@@ -109,7 +91,7 @@ export function applyNightlyChoice(enabled, options = {}) {
109
91
  before,
110
92
  after,
111
93
  log: ok
112
- ? `Nightly refresh is ${desired}; verified from ${after.artifact.path}.`
94
+ ? `Nightly refresh is ${desired}; ${after.evidence}.`
113
95
  : `Nightly refresh did not reach ${desired}: ${run.error?.message || run.stderr?.trim() || run.stdout?.trim() || `exit ${run.status}`}`,
114
96
  };
115
97
  }