ruvnet-brain 4.3.21 → 4.3.25

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 (141) hide show
  1. package/README.md +5 -5
  2. package/bin/install.mjs +275 -60
  3. package/console/app.js +141 -9
  4. package/console/index.html +51 -24
  5. package/console/scope.css +137 -0
  6. package/console/scope.html +144 -0
  7. package/console/scope.js +209 -0
  8. package/console/tips.html +1 -0
  9. package/kb/corpus-release-identity.mjs +239 -0
  10. package/kb/update-storage-transaction.mjs +20 -3
  11. package/package.json +9 -2
  12. package/plugin/.claude-plugin/plugin.json +2 -2
  13. package/plugin/.codex-plugin/plugin.json +1 -1
  14. package/plugin/commands/checkpoint.md +61 -0
  15. package/plugin/hooks/codex-hooks.json +64 -1
  16. package/plugin/hooks/hook-contracts.json +299 -6
  17. package/plugin/hooks/hooks.json +81 -1
  18. package/plugin/mcp/server.mjs +23 -0
  19. package/plugin/scripts/advocacy-catalog.mjs +245 -0
  20. package/plugin/scripts/advocacy-route.mjs +460 -0
  21. package/plugin/scripts/continuation-gate.mjs +25 -2
  22. package/plugin/scripts/continuation-objective.mjs +7 -1
  23. package/plugin/scripts/continuity-hook-policy.mjs +190 -15
  24. package/plugin/scripts/coverage-integrity.mjs +7 -0
  25. package/plugin/scripts/gates.mjs +113 -10
  26. package/plugin/scripts/grounding-turn-gate.mjs +167 -0
  27. package/plugin/scripts/grounding-turn-mark.mjs +91 -0
  28. package/plugin/scripts/hook-shim.mjs +14 -0
  29. package/plugin/scripts/nightly-scheduler.mjs +37 -4
  30. package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
  31. package/plugin/scripts/project-progression-contract.mjs +16 -0
  32. package/plugin/scripts/project-progression-hook.mjs +3 -0
  33. package/plugin/scripts/project-progression-producer.mjs +252 -0
  34. package/plugin/scripts/project-progression-reader.mjs +271 -0
  35. package/plugin/scripts/project-progression-session-start.mjs +93 -16
  36. package/plugin/scripts/project-progression-sources.mjs +220 -0
  37. package/plugin/scripts/project-progression-store.mjs +106 -13
  38. package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
  39. package/plugin/scripts/session-snapshot-hook.mjs +115 -7
  40. package/plugin/scripts/session-start-budget.mjs +59 -0
  41. package/plugin/scripts/session-start-core.mjs +234 -457
  42. package/plugin/scripts/session-start-fsutil.mjs +61 -0
  43. package/plugin/scripts/session-start-health.mjs +64 -0
  44. package/plugin/scripts/session-start-hook-description.mjs +45 -0
  45. package/plugin/scripts/session-start-issue-alert.mjs +77 -0
  46. package/plugin/scripts/session-start-repo-identity.mjs +54 -0
  47. package/plugin/scripts/session-start-signals.mjs +73 -0
  48. package/plugin/scripts/session-start-trace.mjs +86 -0
  49. package/plugin/scripts/session-start-update-plane.mjs +104 -0
  50. package/plugin/scripts/unprompted-runtime.mjs +32 -2
  51. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
  52. package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
  53. package/scripts/adr-072-completion.mjs +1 -1
  54. package/scripts/agentdb-fleet-doctor.mjs +5 -1
  55. package/scripts/approved-runtime.mjs +197 -0
  56. package/scripts/brain-novice-50.mjs +16 -1
  57. package/scripts/brain-score.mjs +23 -5
  58. package/scripts/build-bundle.mjs +971 -530
  59. package/scripts/build-concepts.mjs +36 -116
  60. package/scripts/console-engine.test.mjs +8 -7
  61. package/scripts/console-runtime-identity.mjs +4 -0
  62. package/scripts/corpus-aggregates.mjs +94 -77
  63. package/scripts/corpus-candidate.mjs +475 -222
  64. package/scripts/corpus-next-seed.mjs +225 -0
  65. package/scripts/corpus-promotion.mjs +58 -0
  66. package/scripts/corpus-reconcile.mjs +411 -105
  67. package/scripts/doc-currency.mjs +16 -1
  68. package/scripts/dual-host-deliberation.mjs +25 -2
  69. package/scripts/dual-host-suggest.mjs +17 -1
  70. package/scripts/falsify.mjs +13 -3
  71. package/scripts/gist-receipts.mjs +482 -87
  72. package/scripts/github-health-watch.mjs +12 -2
  73. package/scripts/handoff-asset.mjs +34 -0
  74. package/scripts/hook-retirement-check.mjs +8 -1
  75. package/scripts/host-registry.mjs +1 -1
  76. package/scripts/ingest-gists.mjs +74 -101
  77. package/scripts/job-heartbeat.sh +77 -14
  78. package/scripts/learning-replay-execution.mjs +10 -4
  79. package/scripts/nightly-gists.sh +27 -13
  80. package/scripts/nightly-two-run-proof.mjs +1 -1
  81. package/scripts/nightly-watchdog.mjs +61 -4
  82. package/scripts/onboarding-console.mjs +319 -27
  83. package/scripts/oracle/produce-questions.mjs +293 -0
  84. package/scripts/oracle/producer-hosts.mjs +235 -0
  85. package/scripts/oracle/repo-recall.mjs +448 -0
  86. package/scripts/oracle/retrieval-accuracy.mjs +818 -0
  87. package/scripts/oracle/source-tree.mjs +165 -0
  88. package/scripts/oracle/source-units.mjs +391 -0
  89. package/scripts/oracle/spike-run.mjs +98 -0
  90. package/scripts/oracle/unit-inventory.mjs +141 -0
  91. package/scripts/oracle/unit-sampling.mjs +128 -0
  92. package/scripts/oracle/validate-labels.mjs +250 -0
  93. package/scripts/private-overlay.mjs +248 -0
  94. package/scripts/product-integrity-contract.mjs +1 -1
  95. package/scripts/proxy/claude-proxied.sh +6 -0
  96. package/scripts/proxy/proxy-revert.sh +5 -0
  97. package/scripts/proxy/proxy-up.sh +6 -0
  98. package/scripts/proxy/proxy-verify.mjs +4 -0
  99. package/scripts/public-inputs.mjs +409 -0
  100. package/scripts/public-verification-inputs.mjs +112 -26
  101. package/scripts/public-verification-lane.mjs +1 -1
  102. package/scripts/published-surface-probe.mjs +34 -4
  103. package/scripts/qe/card-lane-gate.mjs +16 -1
  104. package/scripts/qe/session-start-gate.mjs +16 -1
  105. package/scripts/rebuild-gists-from-receipts.mjs +58 -78
  106. package/scripts/record-lesson.mjs +4 -1
  107. package/scripts/rehearse-corpus-pipeline.mjs +994 -0
  108. package/scripts/release-abort-stale.mjs +5 -1
  109. package/scripts/release-authority.mjs +104 -12
  110. package/scripts/release-channel-kind.mjs +86 -0
  111. package/scripts/release-convergence-watchdog.mjs +7 -2
  112. package/scripts/release-projection.mjs +177 -72
  113. package/scripts/release-transaction-provider.mjs +23 -6
  114. package/scripts/release.mjs +252 -17
  115. package/scripts/retrieval-canary.mjs +87 -0
  116. package/scripts/rvf-index-audit.mjs +573 -13
  117. package/scripts/rvf-wire.mjs +269 -0
  118. package/scripts/seal-gist-receipt.mjs +65 -0
  119. package/scripts/selfcheck.mjs +42 -21
  120. package/scripts/source-coverage.mjs +253 -24
  121. package/scripts/status-honesty.mjs +25 -0
  122. package/scripts/sync-census.mjs +0 -0
  123. package/scripts/sync-version.mjs +2 -0
  124. package/scripts/trismart.mjs +42 -0
  125. package/scripts/updater-manifest.mjs +162 -0
  126. package/scripts/verify-channels.mjs +17 -5
  127. package/scripts/wired-check.mjs +48 -10
  128. package/tri-smart-skill/QUICKSTART.md +37 -0
  129. package/tri-smart-skill/README.md +92 -0
  130. package/tri-smart-skill/install.cmd +14 -0
  131. package/tri-smart-skill/install.command +13 -0
  132. package/tri-smart-skill/install.mjs +51 -0
  133. package/tri-smart-skill/install.sh +9 -0
  134. package/tri-smart-skill/tri-smart/SKILL.md +90 -0
  135. package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
  136. package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
  137. package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
  138. package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
  139. package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
  140. package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
  141. package/scripts/corpus-seed-publish.mjs +0 -110
@@ -0,0 +1,269 @@
1
+ // rvf-wire.mjs — read-only readers for the RVF on-disk format, used by the C2 store audit
2
+ // (scripts/rvf-index-audit.mjs) to recover the vectors the engine actually stores so exact
3
+ // nearest neighbours can be computed without trusting the index under test.
4
+ //
5
+ // Why this exists: the installed engine (@ruvector/rvf 0.3.4 over @ruvector/rvf-node 0.2.3)
6
+ // exposes no read-vector-by-id or scan API on either layer — verified by enumerating
7
+ // `Object.getOwnPropertyNames(RvfDatabase.prototype)` on both (see rvf-index-audit.mjs). The only
8
+ // way to obtain the stored vectors is to read VEC_SEG payloads directly.
9
+ //
10
+ // Every layout below is transcribed from rUv's source (ruvector monorepo, crates/rvf):
11
+ // segment header (64 B, LE) ......... rvf-types/src/segment.rs:10-42; constants.rs:4 (magic), :19 (size)
12
+ // header byte positions ............. rvf-runtime/src/write_path.rs:472-490 (header_to_bytes)
13
+ // segment type ids .................. rvf-types/src/segment_type.rs (Vec 0x01, Index 0x02, Journal 0x04,
14
+ // Manifest 0x05, Witness 0x0A) — NOT the 0x00-based table in the npm README
15
+ // content hash ...................... rvf-runtime/src/hashing.rs:22-33 (IEEE CRC32 rotated 0/8/16/24 bits
16
+ // into four LE lanes; checksum_algo 0)
17
+ // VEC_SEG payload ................... rvf-runtime/src/write_path.rs:48-70, read_path.rs:289-330
18
+ // (dim u16, count u32, then [id u64, f32 x dim])
19
+ // manifest payload .................. rvf-runtime/src/read_path.rs:150-256 (epoch u32, dim u16, total u64,
20
+ // seg_count u32, profile u8, metric u8, pad u16, dir entries 25 B each,
21
+ // del_count u32, deleted ids u64 each)
22
+ // journal payload ................... rvf-runtime/src/write_path.rs:88-108 (entry_count u32, epoch u32,
23
+ // prev u64, entries [type u8, pad u8, len u16, id u64])
24
+ // metric ids ........................ rvf-runtime/src/options.rs:42-48 (1 inner_product, 2 cosine, else l2)
25
+ // deletion authority ................ rvf-runtime/src/store.rs:2484 (deletion bitmap = manifest.deleted_ids)
26
+ // Empirically confirmed against the installed binary (2026-09-13): header walk agrees with
27
+ // segments(), VEC payload round-trips ingested vectors bit-exactly, re-ingesting an existing id
28
+ // appends a second entry with the SAME label and the engine serves the LAST one, and
29
+ // status().totalVectors === (distinct VEC labels) - (manifest deleted ids) on stores with deletes.
30
+ import fs from 'node:fs';
31
+
32
+ export const SEGMENT_MAGIC = 0x52564653;
33
+ export const SEGMENT_HEADER_SIZE = 64;
34
+ export const SEG_TYPE = Object.freeze({ VEC: 0x01, INDEX: 0x02, JOURNAL: 0x04, MANIFEST: 0x05, WITNESS: 0x0a });
35
+ const SEG_TYPE_NAME = Object.freeze({ 0x01: 'vec', 0x02: 'index', 0x04: 'journal', 0x05: 'manifest', 0x0a: 'witness' });
36
+ const MAX_SAFE_LABEL = Number.MAX_SAFE_INTEGER;
37
+
38
+ const CRC_TABLE = (() => {
39
+ const table = new Uint32Array(256);
40
+ for (let n = 0; n < 256; n++) {
41
+ let c = n;
42
+ for (let bit = 0; bit < 8; bit++) c = c & 1 ? (c >>> 1) ^ 0xedb88320 : c >>> 1;
43
+ table[n] = c >>> 0;
44
+ }
45
+ return table;
46
+ })();
47
+
48
+ /** IEEE CRC32 (poly 0xEDB88320, init 0xFFFFFFFF, final XOR) — hashing.rs:39-54's reference. */
49
+ export function crc32(buffer) {
50
+ let crc = 0xffffffff;
51
+ for (let i = 0; i < buffer.length; i++) crc = CRC_TABLE[(crc ^ buffer[i]) & 0xff] ^ (crc >>> 8);
52
+ return (crc ^ 0xffffffff) >>> 0;
53
+ }
54
+
55
+ /** hashing.rs:25-33 — the CRC rotated left by 0/8/16/24 bits, each written as a LE u32 lane. */
56
+ export function legacyContentHash(payload) {
57
+ const crc = crc32(payload);
58
+ const out = Buffer.alloc(16);
59
+ for (let lane = 0; lane < 4; lane++) {
60
+ const shift = lane * 8;
61
+ const rotated = shift ? ((crc << shift) | (crc >>> (32 - shift))) >>> 0 : crc;
62
+ out.writeUInt32LE(rotated, lane * 4);
63
+ }
64
+ return out;
65
+ }
66
+
67
+ export function metricFromId(id) {
68
+ return id === 1 ? 'dotproduct' : id === 2 ? 'cosine' : 'l2';
69
+ }
70
+
71
+ function readExact(fd, length, position) {
72
+ const buffer = Buffer.alloc(length);
73
+ let done = 0;
74
+ while (done < length) {
75
+ const n = fs.readSync(fd, buffer, done, length - done, position + done);
76
+ if (n === 0) throw new Error(`unexpected end of file at ${position + done}`);
77
+ done += n;
78
+ }
79
+ return buffer;
80
+ }
81
+
82
+ function readSegmentHeader(fd, offset) {
83
+ const h = readExact(fd, SEGMENT_HEADER_SIZE, offset);
84
+ return {
85
+ magic: h.readUInt32LE(0),
86
+ version: h.readUInt8(4),
87
+ segType: h.readUInt8(5),
88
+ flags: h.readUInt16LE(6),
89
+ segmentId: Number(h.readBigUInt64LE(8)),
90
+ payloadLength: Number(h.readBigUInt64LE(16)),
91
+ checksumAlgo: h.readUInt8(32),
92
+ compression: h.readUInt8(33),
93
+ contentHash: h.subarray(40, 56),
94
+ };
95
+ }
96
+
97
+ function readLabel(buffer, offset) {
98
+ const label = buffer.readBigUInt64LE(offset);
99
+ if (label > BigInt(MAX_SAFE_LABEL)) throw new Error(`vector label ${label} exceeds Number.MAX_SAFE_INTEGER`);
100
+ return Number(label);
101
+ }
102
+
103
+ function parseVecPayload(payload) {
104
+ if (payload.length < 6) throw new Error('VEC_SEG payload shorter than its 6-byte header');
105
+ const dimension = payload.readUInt16LE(0);
106
+ const count = payload.readUInt32LE(2);
107
+ const bytesPerVector = 8 + dimension * 4;
108
+ if (payload.length < 6 + count * bytesPerVector) throw new Error(`VEC_SEG payload truncated: ${payload.length} < ${6 + count * bytesPerVector}`);
109
+ const rows = new Array(count);
110
+ let offset = 6;
111
+ for (let i = 0; i < count; i++) {
112
+ const label = readLabel(payload, offset);
113
+ offset += 8;
114
+ // Float32Array needs 4-byte alignment; entry payloads sit at 6 + 8 (mod 4 = 2), so copy.
115
+ const aligned = new ArrayBuffer(dimension * 4);
116
+ new Uint8Array(aligned).set(payload.subarray(offset, offset + dimension * 4));
117
+ offset += dimension * 4;
118
+ rows[i] = { label, vector: new Float32Array(aligned) };
119
+ }
120
+ return { dimension, count, rows };
121
+ }
122
+
123
+ function parseManifestPayload(payload) {
124
+ if (payload.length < 22) throw new Error('manifest payload shorter than 22 bytes');
125
+ const segCount = payload.readUInt32LE(14);
126
+ if (segCount > Math.floor((payload.length - 22) / 25)) throw new Error('manifest segment directory exceeds payload');
127
+ const directory = [];
128
+ let offset = 22;
129
+ for (let i = 0; i < segCount; i++) {
130
+ directory.push({
131
+ segmentId: Number(payload.readBigUInt64LE(offset)),
132
+ offset: Number(payload.readBigUInt64LE(offset + 8)),
133
+ payloadLength: Number(payload.readBigUInt64LE(offset + 16)),
134
+ segType: payload.readUInt8(offset + 24),
135
+ });
136
+ offset += 25;
137
+ }
138
+ const deletedIds = [];
139
+ if (offset + 4 <= payload.length) {
140
+ const deletedCount = payload.readUInt32LE(offset);
141
+ offset += 4;
142
+ for (let i = 0; i < deletedCount && offset + 8 <= payload.length; i++) {
143
+ deletedIds.push(readLabel(payload, offset));
144
+ offset += 8;
145
+ }
146
+ }
147
+ return {
148
+ epoch: payload.readUInt32LE(0),
149
+ dimension: payload.readUInt16LE(4),
150
+ profileId: payload.readUInt8(18),
151
+ metricId: payload.readUInt8(19),
152
+ metric: metricFromId(payload.readUInt8(19)),
153
+ directory,
154
+ deletedIds,
155
+ };
156
+ }
157
+
158
+ function parseJournalPayload(payload) {
159
+ if (payload.length < 16) throw new Error('journal payload shorter than 16 bytes');
160
+ const entryCount = payload.readUInt32LE(0);
161
+ const deletedIds = [];
162
+ let offset = 16;
163
+ for (let i = 0; i < entryCount && offset + 12 <= payload.length; i++) {
164
+ const entryType = payload.readUInt8(offset);
165
+ if (entryType === 0x01) deletedIds.push(readLabel(payload, offset + 4));
166
+ offset += 12;
167
+ }
168
+ return { epoch: payload.readUInt32LE(4), deletedIds };
169
+ }
170
+
171
+ /**
172
+ * Read every segment the engine lists (`RvfDatabase#segments()` — the authoritative directory, since
173
+ * segments are NOT 64-byte aligned on disk) and return the live stored vectors plus structural failures.
174
+ *
175
+ * Live set = last VEC entry per label (engine behaviour, see header) minus the latest manifest's
176
+ * deleted ids (store.rs:2484) unioned with any JOURNAL tombstones. Callers compare
177
+ * `vectors.size` with `status().totalVectors`.
178
+ */
179
+ export function readRvfStoredVectors(rvfPath, listedSegments) {
180
+ const failures = [];
181
+ const vectors = new Map();
182
+ const deleted = new Set();
183
+ let dimension = null;
184
+ let entries = 0;
185
+ let duplicateLabels = 0;
186
+ let manifest = null;
187
+ let hashesVerified = 0;
188
+ const fd = fs.openSync(rvfPath, 'r');
189
+ try {
190
+ const fileSize = fs.fstatSync(fd).size;
191
+ for (const listed of listedSegments) {
192
+ const where = `${listed.segType}#${listed.id}@${listed.offset}`;
193
+ if (listed.offset + SEGMENT_HEADER_SIZE + listed.payloadLength > fileSize) {
194
+ failures.push({ kind: 'segment-beyond-eof', segment: where, detail: `file is ${fileSize} bytes` });
195
+ continue;
196
+ }
197
+ const header = readSegmentHeader(fd, listed.offset);
198
+ const expectedName = SEG_TYPE_NAME[header.segType];
199
+ if (header.magic !== SEGMENT_MAGIC || header.payloadLength !== listed.payloadLength
200
+ || (expectedName && expectedName !== listed.segType)) {
201
+ failures.push({ kind: 'segment-header-mismatch', segment: where, detail: `magic=0x${header.magic.toString(16)} type=${header.segType} payloadLength=${header.payloadLength}` });
202
+ continue;
203
+ }
204
+ if (header.compression !== 0) {
205
+ failures.push({ kind: 'compressed-segment-unsupported', segment: where, detail: `compression=${header.compression}` });
206
+ continue;
207
+ }
208
+ const payload = readExact(fd, header.payloadLength, listed.offset + SEGMENT_HEADER_SIZE);
209
+ if (!legacyContentHash(payload).equals(header.contentHash)) {
210
+ failures.push({ kind: 'segment-content-hash-mismatch', segment: where, detail: 'payload bytes do not hash to the header content_hash (hashing.rs:22-33)' });
211
+ continue;
212
+ }
213
+ hashesVerified++;
214
+ try {
215
+ if (header.segType === SEG_TYPE.VEC) {
216
+ const vec = parseVecPayload(payload);
217
+ if (dimension === null) dimension = vec.dimension;
218
+ else if (vec.dimension !== dimension) failures.push({ kind: 'dimension-mismatch', segment: where, detail: `${vec.dimension} != ${dimension}` });
219
+ for (const row of vec.rows) {
220
+ entries++;
221
+ if (vectors.has(row.label)) duplicateLabels++;
222
+ vectors.set(row.label, row.vector);
223
+ }
224
+ } else if (header.segType === SEG_TYPE.MANIFEST) {
225
+ manifest = parseManifestPayload(payload);
226
+ } else if (header.segType === SEG_TYPE.JOURNAL) {
227
+ for (const id of parseJournalPayload(payload).deletedIds) deleted.add(id);
228
+ }
229
+ } catch (error) {
230
+ failures.push({ kind: 'segment-payload-malformed', segment: where, detail: error.message });
231
+ }
232
+ }
233
+ } finally {
234
+ fs.closeSync(fd);
235
+ }
236
+ let supersededSegments = 0;
237
+ if (manifest) {
238
+ for (const id of manifest.deletedIds) deleted.add(id);
239
+ // A manifest directory is a point-in-time snapshot, so a directory entry the engine no longer
240
+ // lists is normal: rebuilding the HNSW appends a new INDEX_SEG and supersedes the old one, which
241
+ // stays on disk as dead space (measured on the live corpus: ruvector's last manifest still points
242
+ // at index seg 331 @87206459 while segments() serves index 338 @96475134, deadSpaceRatio 0.0245).
243
+ // What is NOT acceptable is a DANGLING entry — one whose bytes are gone, truncated, or no longer
244
+ // a well-formed segment of the declared type. Only that is reported as a failure.
245
+ const listedByOffset = new Map(listedSegments.map((segment) => [segment.offset, segment]));
246
+ const fd2 = fs.openSync(rvfPath, 'r');
247
+ try {
248
+ const fileSize = fs.fstatSync(fd2).size;
249
+ for (const entry of manifest.directory) {
250
+ const seen = listedByOffset.get(entry.offset);
251
+ if (seen && seen.payloadLength === entry.payloadLength) continue;
252
+ if (entry.offset + SEGMENT_HEADER_SIZE + entry.payloadLength > fileSize) {
253
+ failures.push({ kind: 'manifest-directory-dangling', detail: `manifest points at segment type=${entry.segType} offset=${entry.offset} length=${entry.payloadLength}, past the ${fileSize}-byte end of file` });
254
+ continue;
255
+ }
256
+ const header = readSegmentHeader(fd2, entry.offset);
257
+ if (header.magic !== SEGMENT_MAGIC || header.segType !== entry.segType || header.payloadLength !== entry.payloadLength) {
258
+ failures.push({ kind: 'manifest-directory-dangling', detail: `manifest points at segment type=${entry.segType} offset=${entry.offset} length=${entry.payloadLength}, but those bytes are magic=0x${header.magic.toString(16)} type=${header.segType} length=${header.payloadLength}` });
259
+ continue;
260
+ }
261
+ supersededSegments++;
262
+ }
263
+ } finally {
264
+ fs.closeSync(fd2);
265
+ }
266
+ }
267
+ for (const id of deleted) vectors.delete(id);
268
+ return { dimension, vectors, entries, duplicateLabels, deleted, manifest, hashesVerified, supersededSegments, segmentCount: listedSegments.length, failures };
269
+ }
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ // Seals a complete, current ruv-gists.sources.json (schema 3) covering every gist rUv has
3
+ // published, embeds the corresponding ruv-gists RVF, and validates the whole result -- using ONLY
4
+ // the repo's single canonical gist pipeline: observeSourceUniverse (source-coverage.mjs, fresh live
5
+ // listing) -> buildGistAggregate (gist-receipts.mjs, capture + render + embed + seal + validate,
6
+ // atomically). There is no independent binding step any more: a receipt can never be sealed with
7
+ // passagesSha256:null, because buildGistAggregate renders and hashes the passage bytes in the same
8
+ // call that seals the receipt.
9
+ //
10
+ // node scripts/seal-gist-receipt.mjs [--assets kb] [--policy kb] [--owner ruvnet]
11
+ import path from 'node:path';
12
+ import fs from 'node:fs';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { observeSourceUniverse } from './source-coverage.mjs';
15
+ import { buildGistAggregate } from './gist-receipts.mjs';
16
+
17
+ export const REPO_KB = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'kb');
18
+
19
+ export function arg(argv, name, fallback) {
20
+ const i = argv.indexOf(name);
21
+ return i >= 0 && argv[i + 1] !== undefined ? argv[i + 1] : fallback;
22
+ }
23
+
24
+ // MUST mirror source-coverage.mjs main()'s OWN policyDir resolution exactly (see there:
25
+ // `policyDir = assetsIndex >= 0 ? kbDir : path.join(ROOT, 'kb')`) -- policy (external-sources.json,
26
+ // no-corpus-repos.json) is NOT the same directory as assets whenever assets points somewhere other
27
+ // than the repo's own kb/ (e.g. the live installed ~/.cache/ruvnet-brain/kb). The sealed receipt's
28
+ // sourceObservationSha256 is compared byte-for-byte against whatever buildCoverage computes later,
29
+ // and a policy-dir mismatch silently changes the REPOSITORY half of that combined hash (gists are
30
+ // fine) so every gist reads FAILED with "observation differs from coverage" even though nothing
31
+ // about the gists themselves is wrong. Found live 2026-09-12: assetsDir was the install cache,
32
+ // which has no external-sources.json of its own, so this file's old default (policy==assets) fell
33
+ // back to an empty external list -- 218 repos instead of buildCoverage's real 228. Default policy
34
+ // to the repo's kb/ (source-coverage.mjs's own default), never to assetsDir.
35
+ export function resolvePolicyDir(argv, repoKb = REPO_KB) {
36
+ return arg(argv, '--policy', repoKb);
37
+ }
38
+
39
+ async function main(argv = process.argv.slice(2)) {
40
+ const assetsDir = arg(argv, '--assets', 'kb');
41
+ const owner = arg(argv, '--owner', 'ruvnet');
42
+ const policyDir = resolvePolicyDir(argv);
43
+ const externalPath = path.join(path.resolve(policyDir), 'external-sources.json');
44
+ const externalSources = fs.existsSync(externalPath)
45
+ ? JSON.parse(fs.readFileSync(externalPath, 'utf8')).sources : [];
46
+ const observation = observeSourceUniverse({ owner, externalSources });
47
+ console.log(`live gists observed: ${observation.gists.rows.length}`);
48
+ const result = await buildGistAggregate({ observation, outDir: assetsDir });
49
+ if (result.omitted) {
50
+ console.log(`[seal-gist-receipt] @${owner} currently has zero public gists -- aggregate omitted, nothing sealed.`);
51
+ return 0;
52
+ }
53
+ console.log(`sealed gist count: ${Object.keys(result.sourceReceipt.gists).length}`);
54
+ console.log(`passagesSha256: ${result.sourceReceipt.passagesSha256}`);
55
+ console.log(`receiptSha256: ${result.sourceReceipt.receiptSha256}`);
56
+ console.log(`reused ${result.reuseEvidence.reused.length} cached, fetched ${result.reuseEvidence.fetched.length} fresh gist(s)`);
57
+ return 0;
58
+ }
59
+
60
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
61
+ main().then((code) => { process.exitCode = code; }).catch((error) => {
62
+ console.error(`[seal-gist-receipt] ${error.message}`);
63
+ process.exitCode = 1;
64
+ });
65
+ }
@@ -64,6 +64,9 @@ import path from 'node:path';
64
64
  import os from 'node:os';
65
65
  import { spawn, spawnSync } from 'node:child_process';
66
66
  import { fileURLToPath } from 'node:url';
67
+ // THE canonical continuity policy — the same module bin/install.mjs and scripts/hook-retirement-check.mjs
68
+ // import. Never re-implement this predicate locally; see the note below the imports.
69
+ import { isAllowedContinuityRegistration } from '../plugin/scripts/continuity-hook-policy.mjs';
67
70
 
68
71
  // ── contract constants (ADR-053 §2.5 / §2.3) ────────────────────────────────────────────────────
69
72
  export const STDOUT_CAP_BYTES = 4096; // it lands in the user's context window
@@ -73,26 +76,31 @@ export const WATCHDOG_GRACE_MS = 2000; // SIGTERM → this long → hard kill, t
73
76
  /** Advisory hooks may only ever exit 0. Blocking hooks may exit 0, 1 or 2 (ADR-023's table). */
74
77
  export const ALLOWED_EXITS = Object.freeze({ advisory: [0], blocking: [0, 1, 2] });
75
78
 
76
- // Keep this predicate local: selfcheck is copied into isolated mutation fixtures and must remain
77
- // runnable when only this one file is present. The shipped registry policy is the authority for
78
- // manifests; this duplicate is deliberately limited to classifying the two allowed lifecycle rows.
79
- function isAllowedContinuityRegistration({ event, matcher, command } = {}, contracts = []) {
80
- const text = String(command || '');
81
- if (!/(?:hook-shim\.mjs|codex-hook\.mjs)/i.test(text)) return false;
82
- const id = event === 'SessionStart' && String(matcher ?? '') === 'startup|resume|clear|compact|fork'
83
- ? 'session-start'
84
- : event === 'Stop' && String(matcher ?? '') === '*'
85
- ? 'continuation-gate'
86
- : null;
87
- if (id && new RegExp(`(?:^|[\\s"'])${id}(?:$|[\\s"'])`).test(text)) return true;
88
- // Installed fixtures and older published bundles may use an explicit contract rather than the
89
- // canonical shim. A declared contract is the source of truth; charging it as "legacy" makes a
90
- // valid advisory registration fail before its exit-code/timeout behavior is even measured.
91
- return contracts.some((contract) => typeof contract?.commandIncludes === 'string'
92
- && text.includes(contract.commandIncludes)
93
- && (!contract.event || contract.event === event)
94
- && (contract.matcher == null || String(contract.matcher) === String(matcher)));
95
- }
79
+ // THE SECOND COPY OF THE CONTINUITY POLICY — DELETED 2026-09-15, and this is why.
80
+ //
81
+ // This file used to carry its OWN isAllowedContinuityRegistration(), hardcoding the two ids
82
+ // 'session-start' and 'continuation-gate'. That was correct when the plane really was retired to
83
+ // continuity-only (00526b12, 2026-09-07). The project then deliberately grew the plane back to
84
+ // ELEVEN registrations (9c45d408 restored the lifecycle, 7b8e6e73 wired the search-first gate,
85
+ // ef2b8e12 extended it to Codex). plugin/scripts/continuity-hook-policy.mjs was updated. This
86
+ // duplicate was not.
87
+ //
88
+ // WHAT THAT COST: `npm run hooks:check` and bin/install.mjs both import the canonical policy, so
89
+ // both reported the plane compliant -- while THIS file, which runs unconditionally at the end of
90
+ // every `npx ruvnet-brain` install and every --doctor, classified 9 of the 11 shipped registrations
91
+ // as LEGACY and exited 1. Reproduced byte-for-byte outside CI against an `installed:<version>`
92
+ // surface -- the directory a SUCCESSFUL `claude plugin install` creates. The more correctly the
93
+ // install succeeded, the more certainly it reported failure. Only the stranger matrix could see it,
94
+ // and stranger had been skipped for a month behind a red `ci` job.
95
+ //
96
+ // The old comment justified the duplicate as "selfcheck is copied into isolated mutation fixtures
97
+ // and must remain runnable when only this one file is present". That is no longer true and is
98
+ // verified, not assumed: tests/mutation/install-selfcheck-consumption-mutation.test.mjs stages
99
+ // CONSOLE_RUNTIME_SURFACE, which lists 'plugin/scripts' (scripts/console-runtime-identity.mjs:34),
100
+ // so the canonical policy is already present in that fixture.
101
+ //
102
+ // ONE policy, ONE source of truth. A checker and its subject that disagree guarantee recurring red
103
+ // builds -- the same lesson scripts/repo-count-detector.mjs was extracted to record.
96
104
 
97
105
  /**
98
106
  * Load hook-registry.mjs. It is a sibling in `scripts/`, shipped alongside this file — see the
@@ -602,6 +610,10 @@ export async function checkCoexistence({ home = os.homedir(), repo = null } = {}
602
610
  * "not available" line and score nothing — a security check that invents a pass is worse than none.
603
611
  */
604
612
  export function runSecurityScan({ cwd = process.cwd(), timeoutMs = 20000, env = process.env } = {}) {
613
+ // Every `ruflo` invocation auto-starts a project background daemon unless this is set (verified
614
+ // live against the installed CLI: ~/.npm-global/lib/node_modules/ruflo/node_modules/@claude-flow/cli/dist/src/services/daemon-autostart.js:85).
615
+ // A read-only version probe + two scan subcommands have no business leaving a daemon running.
616
+ env = { ...env, RUFLO_DAEMON_AUTOSTART: '0' };
605
617
  const probe = spawnSync('ruflo', ['--version'], { encoding: 'utf8', timeout: 5000, env });
606
618
  if (probe.error || probe.status !== 0) {
607
619
  return { available: false, reason: 'ruflo not found on PATH — MCP surface not scanned (install: npm i -g ruflo)' };
@@ -655,10 +667,19 @@ export async function selfCheck({ home = os.homedir(), repo = null, cwd = os.tmp
655
667
  }
656
668
  } catch { /* battery already has the authoritative result */ }
657
669
  const legacyRegistrations = battery.registrations.filter((registration) =>
658
- !isAllowedContinuityRegistration({ ...registration, layer: 'plugin' }, contracts)
670
+ !isAllowedContinuityRegistration({ ...registration, layer: 'plugin' })
671
+ // KEPT DELIBERATELY. The current plugin/hooks/hook-contracts.json carries no
672
+ // `commandIncludes`, so it is tempting to call this dead and delete it -- I did, and
673
+ // tests/mesh/coexistence.test.mjs immediately proved it wrong. `git log -S commandIncludes`
674
+ // shows the field WAS on real contracts until 00526b12 (2026-09-07, the retire-automatic-hooks
675
+ // release), so anyone still installed from a bundle older than that has contracts only this
676
+ // branch can recognise. Charging their valid registration as "legacy" would fail their install
677
+ // for upgrading late. The BUG was never this fallback -- it was the hardcoded two-id list
678
+ // above it, now replaced by the canonical policy.
659
679
  && !(battery.surface.source.startsWith('installed:')
660
680
  && contracts.some((contract) => typeof contract?.commandIncludes === 'string'
661
681
  && String(registration.command || '').includes(contract.commandIncludes))));
682
+
662
683
  if (legacyRegistrations.length !== 0) {
663
684
  violations.push({
664
685
  kind: 'automatic-registration',