ruvnet-brain 4.3.25 → 4.3.27
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 +2 -2
- package/bin/install.mjs +19 -4
- package/console/app.js +48 -0
- package/console/index.html +19 -0
- package/console/style.css +26 -0
- package/data/model-catalog.json +1 -1
- package/docs/RELEASE-NOTES-4.0.md +4 -3
- package/kb/capability-only.mjs +27 -0
- package/kb/capability-summaries/cognitum-ruos/CAPABILITIES.md +26 -0
- package/kb/verify-citation.mjs +16 -4
- package/package.json +5 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/docs/RELEASE-NOTES-4.0.md +4 -3
- package/plugin/hooks/codex-hooks.json +6 -1
- package/plugin/hooks/hook-contracts.json +24 -2
- package/plugin/hooks/hooks.json +6 -1
- package/plugin/scripts/capacity-aware-parallel-work.mjs +200 -0
- package/plugin/scripts/codex-hook-adapter.mjs +37 -0
- package/plugin/scripts/continuity-hook-policy.mjs +4 -0
- package/plugin/scripts/coverage-integrity.mjs +17 -0
- package/plugin/scripts/hook-shim.mjs +1 -0
- package/plugin/scripts/lesson-gate.mjs +4 -1
- package/plugin/scripts/project-progression-reader.mjs +12 -1
- package/plugin/scripts/project-progression-session-start.mjs +24 -6
- package/plugin/scripts/project-progression-store.mjs +76 -4
- package/plugin/skills/release-proof/SKILL.md +28 -4
- package/plugin/skills/release-proof/scripts/release-proof.mjs +48 -24
- package/scripts/brain-novice-50.mjs +14 -16
- package/scripts/build-bundle.mjs +2 -0
- package/scripts/corpus-dispatch-receipt.mjs +22 -0
- package/scripts/corpus-reconcile.mjs +26 -4
- package/scripts/doc-currency.mjs +12 -4
- package/scripts/eval-brain.mjs +7 -5
- package/scripts/gist-git-transport.mjs +218 -0
- package/scripts/gist-receipts.mjs +168 -38
- package/scripts/ingest-gists.mjs +5 -2
- package/scripts/installed-brain-health.mjs +99 -0
- package/scripts/onboarding-console.mjs +45 -1
- package/scripts/public-inputs.mjs +2 -1
- package/scripts/public-verification-inputs.mjs +14 -6
- package/scripts/refresh-capability-only-store.mjs +143 -0
- package/scripts/release-transaction-provider.mjs +25 -5
- package/scripts/release-transaction.mjs +40 -11
- package/scripts/release-vector.mjs +44 -20
- package/scripts/run-operational-benchmark.mjs +151 -0
- package/scripts/run-operational-benchmark.v3.mjs +194 -0
- package/scripts/self-update.mjs +2 -0
- package/scripts/source-coverage.mjs +6 -1
- package/scripts/sync-version.mjs +10 -2
- package/scripts/wired-check.mjs +73 -45
|
@@ -33,6 +33,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
33
33
|
import { canonicalJson, digest, validateGistAggregateReceipt } from './coverage-integrity.mjs';
|
|
34
34
|
import { promoteArtifactSet } from '../kb/incremental-refresh.mjs';
|
|
35
35
|
import { writeRvfGeneration } from './rvf-generation.mjs';
|
|
36
|
+
import { fetchGistGitSnapshot } from './gist-git-transport.mjs';
|
|
36
37
|
|
|
37
38
|
const DEFAULT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
38
39
|
const TEXT_EXT = new Set(['.md', '.markdown', '.txt', '.rst']);
|
|
@@ -45,6 +46,7 @@ const HEX_GIST = /^[a-f0-9]{20,64}$/;
|
|
|
45
46
|
const OWNER_RE = /^[A-Za-z0-9-]{1,39}$/;
|
|
46
47
|
const compareCanonicalText = (left, right) => String(left) < String(right) ? -1
|
|
47
48
|
: String(left) > String(right) ? 1 : 0;
|
|
49
|
+
const VERIFIED_CAPTURE_SETS = new WeakMap();
|
|
48
50
|
|
|
49
51
|
function validDate(value) {
|
|
50
52
|
return typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
@@ -122,38 +124,67 @@ const RATE_LIMIT_RE = /rate limit/i;
|
|
|
122
124
|
const NOT_FOUND_RE = /\bnot found\b|HTTP 404/i;
|
|
123
125
|
const FORBIDDEN_INTEGRATION_RE = /resource not accessible by integration/i;
|
|
124
126
|
const TRANSIENT_RE = /timeout|timed out|TLS handshake|ECONNRESET|ECONNREFUSED|EAI_AGAIN|temporary failure|HTTP 5\d\d|HTTP 429|socket hang up/i;
|
|
127
|
+
const MAX_RATE_LIMIT_WAIT_MS = 15 * 60 * 1000;
|
|
128
|
+
|
|
129
|
+
function retryDelay(error, attempt, retryDelayMs) {
|
|
130
|
+
if (error?.code !== 'GIST_RATE_LIMITED') return retryDelayMs * attempt;
|
|
131
|
+
const retryAfter = Number(error.headers?.['retry-after']);
|
|
132
|
+
const resetAt = Number(error.headers?.['x-ratelimit-reset']);
|
|
133
|
+
const requested = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000
|
|
134
|
+
: Number.isFinite(resetAt) && resetAt > 0 ? Math.max(0, resetAt * 1000 - Date.now()) : retryDelayMs * attempt;
|
|
135
|
+
return requested > MAX_RATE_LIMIT_WAIT_MS ? null : Math.max(retryDelayMs * attempt, requested);
|
|
136
|
+
}
|
|
125
137
|
|
|
126
138
|
export class GistFetchError extends Error {
|
|
127
|
-
constructor(message, { code, gistId, status, retryable = false } = {}) {
|
|
139
|
+
constructor(message, { code, gistId, status, headers = {}, retryable = false } = {}) {
|
|
128
140
|
super(message);
|
|
129
141
|
this.name = 'GistFetchError';
|
|
130
142
|
this.code = code;
|
|
131
143
|
this.gistId = gistId;
|
|
132
144
|
if (status !== undefined) this.status = status;
|
|
145
|
+
if (headers && Object.keys(headers).length) this.headers = headers;
|
|
133
146
|
this.retryable = retryable;
|
|
134
147
|
}
|
|
135
148
|
}
|
|
136
149
|
|
|
137
|
-
function classifyGistFetchFailure(stderr, gistId) {
|
|
150
|
+
function classifyGistFetchFailure(stderr, gistId, { status, headers = {} } = {}) {
|
|
138
151
|
const message = String(stderr || '').trim();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
152
|
+
const responseStatus = status ?? (Number(message.match(/\bHTTP\s+(\d{3})\b/i)?.[1]) || undefined);
|
|
153
|
+
const safeHeaders = Object.fromEntries(['x-ratelimit-remaining', 'x-ratelimit-reset', 'retry-after', 'x-ratelimit-resource', 'x-github-request-id']
|
|
154
|
+
.flatMap((key) => headers[key] == null ? [] : [[key, String(headers[key])]]));
|
|
155
|
+
const responseDetail = [responseStatus ? `HTTP ${responseStatus}` : '', Object.keys(safeHeaders).length
|
|
156
|
+
? `headers=${JSON.stringify(safeHeaders)}` : '', message].filter(Boolean).join('; ');
|
|
157
|
+
if (responseStatus === 404 || (!responseStatus && NOT_FOUND_RE.test(message))) {
|
|
158
|
+
return new GistFetchError(`gist ${gistId} was moved or deleted: ${responseDetail}`,
|
|
159
|
+
{ code: 'GIST_NOT_FOUND', gistId, status: responseStatus, headers: safeHeaders, retryable: false });
|
|
142
160
|
}
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
161
|
+
if (RATE_LIMIT_RE.test(message) || (responseStatus === 403 && headers['x-ratelimit-remaining'] === '0')
|
|
162
|
+
|| (responseStatus === 429 && Boolean(headers['retry-after']))) {
|
|
163
|
+
return new GistFetchError(`gist ${gistId} fetch rate-limited: ${responseDetail}`,
|
|
164
|
+
{ code: 'GIST_RATE_LIMITED', gistId, status: responseStatus, headers: safeHeaders, retryable: true });
|
|
146
165
|
}
|
|
147
|
-
if (
|
|
148
|
-
return new GistFetchError(`gist ${gistId} fetch
|
|
149
|
-
{ code: '
|
|
166
|
+
if (responseStatus === 403 && (FORBIDDEN_INTEGRATION_RE.test(message) || !headers['x-ratelimit-remaining'])) {
|
|
167
|
+
return new GistFetchError(`gist ${gistId} fetch rejected (HTTP 403): ${responseDetail}`,
|
|
168
|
+
{ code: 'GIST_FORBIDDEN', gistId, status: responseStatus, headers: safeHeaders, retryable: false });
|
|
150
169
|
}
|
|
151
|
-
if (TRANSIENT_RE.test(message)) {
|
|
152
|
-
return new GistFetchError(`gist ${gistId} fetch failed transiently: ${
|
|
153
|
-
{ code: 'GIST_TRANSIENT_FAILURE', gistId, retryable: true });
|
|
170
|
+
if (TRANSIENT_RE.test(message) || (responseStatus >= 500 && responseStatus < 600) || responseStatus === 429) {
|
|
171
|
+
return new GistFetchError(`gist ${gistId} fetch failed transiently: ${responseDetail}`,
|
|
172
|
+
{ code: 'GIST_TRANSIENT_FAILURE', gistId, status: responseStatus, headers: safeHeaders, retryable: true });
|
|
154
173
|
}
|
|
155
|
-
return new GistFetchError(`gist ${gistId} fetch failed: ${
|
|
156
|
-
{ code: 'GIST_FETCH_FAILED', gistId, retryable: false });
|
|
174
|
+
return new GistFetchError(`gist ${gistId} fetch failed: ${responseDetail}`,
|
|
175
|
+
{ code: 'GIST_FETCH_FAILED', gistId, status: responseStatus, headers: safeHeaders, retryable: false });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseGhResponse(output) {
|
|
179
|
+
const text = String(output || '');
|
|
180
|
+
const match = text.match(/^HTTP\/\S+\s+(\d+)\r?\n([\s\S]*?)\r?\n\r?\n([\s\S]*)$/m);
|
|
181
|
+
if (!match) return { status: undefined, headers: {}, body: text };
|
|
182
|
+
const headers = {};
|
|
183
|
+
for (const line of match[2].split(/\r?\n/)) {
|
|
184
|
+
const split = line.indexOf(':');
|
|
185
|
+
if (split > 0) headers[line.slice(0, split).trim().toLowerCase()] = line.slice(split + 1).trim();
|
|
186
|
+
}
|
|
187
|
+
return { status: Number(match[1]), headers, body: match[3] };
|
|
157
188
|
}
|
|
158
189
|
|
|
159
190
|
function defaultSleep(ms) {
|
|
@@ -172,11 +203,14 @@ export async function defaultFetchGist(id, { spawn = spawnSync, retries = 3, ret
|
|
|
172
203
|
const attempts = Math.max(1, retries);
|
|
173
204
|
let lastError;
|
|
174
205
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
175
|
-
const result = spawn('gh', ['api', `gists/${id}`], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
176
|
-
|
|
177
|
-
|
|
206
|
+
const result = spawn('gh', ['api', '--include', `gists/${id}`], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
207
|
+
const response = parseGhResponse(result.stdout);
|
|
208
|
+
if (result.status === 0) return JSON.parse(response.body);
|
|
209
|
+
lastError = classifyGistFetchFailure(`${result.stderr || ''}\n${response.body || ''}`, id, response);
|
|
178
210
|
if (!lastError.retryable || attempt === attempts) throw lastError;
|
|
179
|
-
|
|
211
|
+
const delay = retryDelay(lastError, attempt, retryDelayMs);
|
|
212
|
+
if (delay === null) throw lastError;
|
|
213
|
+
await sleep(delay);
|
|
180
214
|
if (signal?.aborted) throw abortError(signal);
|
|
181
215
|
}
|
|
182
216
|
throw lastError;
|
|
@@ -185,17 +219,34 @@ export async function defaultFetchGist(id, { spawn = spawnSync, retries = 3, ret
|
|
|
185
219
|
// The unauthenticated fallback used ONLY for the specific 403 "no gist scope" rejection. Mirrors
|
|
186
220
|
// source-coverage.mjs's listGistsUnauthenticated: same env var, same accept header, same
|
|
187
221
|
// unauthenticated-quota reasoning. `fetchImpl` is a test seam (default: global fetch).
|
|
188
|
-
async function fetchPublicGistDetail(id, { fetchImpl = globalThis.fetch,
|
|
222
|
+
async function fetchPublicGistDetail(id, { fetchImpl = globalThis.fetch, retries = 3, retryDelayMs = 300,
|
|
223
|
+
sleep = defaultSleep, signal } = {}) {
|
|
189
224
|
const apiBase = process.env.RUVNET_GISTS_API || 'https://api.github.com';
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
225
|
+
const attempts = Math.max(1, retries);
|
|
226
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
227
|
+
const response = await fetchImpl(`${apiBase}/gists/${id}`, {
|
|
228
|
+
headers: { accept: 'application/vnd.github+json', 'user-agent': 'ruvnet-brain-gist-receipts' },
|
|
229
|
+
signal,
|
|
230
|
+
});
|
|
231
|
+
if (response.ok) return response.json();
|
|
232
|
+
const headers = {};
|
|
233
|
+
for (const key of ['x-ratelimit-remaining', 'x-ratelimit-reset', 'retry-after', 'x-ratelimit-resource', 'x-github-request-id']) {
|
|
234
|
+
const value = response.headers?.get?.(key);
|
|
235
|
+
if (value != null) headers[key] = value;
|
|
236
|
+
}
|
|
237
|
+
const body = await response.json?.().catch?.(() => null);
|
|
238
|
+
const classified = classifyGistFetchFailure(`public detail fetch failed: HTTP ${response.status} ${body?.message || ''}`, id,
|
|
239
|
+
{ status: response.status, headers });
|
|
240
|
+
const error = new GistFetchError(`gist ${id} public detail fetch failed: ${classified.message}`,
|
|
241
|
+
{ code: classified.code, gistId: id, status: response.status, headers: classified.headers,
|
|
242
|
+
retryable: classified.retryable });
|
|
243
|
+
if (!error.retryable || attempt === attempts) throw error;
|
|
244
|
+
const delay = retryDelay(error, attempt, retryDelayMs);
|
|
245
|
+
if (delay === null) throw error;
|
|
246
|
+
await sleep(delay);
|
|
247
|
+
if (signal?.aborted) throw abortError(signal);
|
|
197
248
|
}
|
|
198
|
-
|
|
249
|
+
throw new Error(`gist ${id} public detail retries exhausted`);
|
|
199
250
|
}
|
|
200
251
|
|
|
201
252
|
// The transport `captureGistSources` uses by default for per-gist DETAIL. Wraps `defaultFetchGist`
|
|
@@ -229,9 +280,18 @@ export async function defaultFetchRaw(file, { fetchImpl = globalThis.fetch, retr
|
|
|
229
280
|
headers: { 'user-agent': 'ruvnet-brain-gist-receipts' }, signal,
|
|
230
281
|
});
|
|
231
282
|
if (response.ok) return Buffer.from(await response.arrayBuffer());
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
283
|
+
const headers = {};
|
|
284
|
+
for (const key of ['x-ratelimit-remaining', 'x-ratelimit-reset', 'retry-after', 'x-ratelimit-resource', 'x-github-request-id']) {
|
|
285
|
+
const value = response.headers?.get?.(key);
|
|
286
|
+
if (value != null) headers[key] = value;
|
|
287
|
+
}
|
|
288
|
+
const rateLimited = response.status === 403 && (headers['x-ratelimit-remaining'] === '0'
|
|
289
|
+
|| RATE_LIMIT_RE.test(response.statusText || '')) || response.status === 429;
|
|
290
|
+
const transient = rateLimited || (response.status >= 500 && response.status < 600);
|
|
291
|
+
lastError = new GistFetchError(`raw gist fetch failed: HTTP ${response.status}; headers=${JSON.stringify(
|
|
292
|
+
Object.fromEntries(Object.entries(headers).filter(([key]) => ['x-ratelimit-remaining', 'x-ratelimit-reset', 'retry-after', 'x-ratelimit-resource', 'x-github-request-id'].includes(key))))}`,
|
|
293
|
+
{ code: rateLimited ? 'GIST_RATE_LIMITED' : transient ? 'GIST_RAW_TRANSIENT_FAILURE' : 'GIST_RAW_FETCH_FAILED',
|
|
294
|
+
status: response.status, headers, retryable: transient });
|
|
235
295
|
if (!transient) throw lastError;
|
|
236
296
|
} catch (error) {
|
|
237
297
|
if (error instanceof GistFetchError) { lastError = error; if (!error.retryable) throw error; }
|
|
@@ -243,7 +303,9 @@ export async function defaultFetchRaw(file, { fetchImpl = globalThis.fetch, retr
|
|
|
243
303
|
}
|
|
244
304
|
}
|
|
245
305
|
if (attempt === attempts) throw lastError;
|
|
246
|
-
|
|
306
|
+
const delay = retryDelay(lastError, attempt, retryDelayMs);
|
|
307
|
+
if (delay === null) throw lastError;
|
|
308
|
+
await sleep(delay);
|
|
247
309
|
if (signal?.aborted) throw abortError(signal);
|
|
248
310
|
}
|
|
249
311
|
throw lastError;
|
|
@@ -340,18 +402,49 @@ function reusableCachedGist(cached, stub) {
|
|
|
340
402
|
return 'reuse';
|
|
341
403
|
}
|
|
342
404
|
|
|
343
|
-
export async function captureGistSources({ observation, cache = null, fetchDetail =
|
|
344
|
-
fetchRaw = defaultFetchRaw,
|
|
405
|
+
export async function captureGistSources({ observation, cache = null, fetchDetail = null,
|
|
406
|
+
fetchRaw = defaultFetchRaw, fetchGitSnapshot = fetchGistGitSnapshot, signal,
|
|
407
|
+
now = () => new Date().toISOString() } = {}) {
|
|
345
408
|
const stubs = observation?.gists?.rows;
|
|
346
409
|
if (!Array.isArray(stubs) || stubs.some(({ id }) => !HEX_GIST.test(String(id || '')))
|
|
347
410
|
|| new Set(stubs.map(({ id }) => id)).size !== stubs.length) {
|
|
348
411
|
throw new Error('source observation has missing or duplicate gist ids');
|
|
349
412
|
}
|
|
413
|
+
const observedRowsSha256 = digest({ owner: observation.owner, observedAt: observation.observedAt, rows: stubs });
|
|
350
414
|
const gists = {};
|
|
351
415
|
const reused = [];
|
|
352
416
|
const fetched = [];
|
|
353
417
|
for (const stub of [...stubs].sort((a, b) => String(a.id).localeCompare(String(b.id)))) {
|
|
354
418
|
if (signal?.aborted) throw abortError(signal);
|
|
419
|
+
if (!fetchDetail) {
|
|
420
|
+
// The production/no-key route never spends one REST detail request per gist and never trusts
|
|
421
|
+
// a body-free cache or timestamps as content identity. A full bare clone gives one immutable
|
|
422
|
+
// Git commit/tree; every observed raw_url's blob identity must match its HEAD file bytes.
|
|
423
|
+
const snapshot = await fetchGitSnapshot({ owner: observation.owner, stub, signal });
|
|
424
|
+
const files = [];
|
|
425
|
+
for (const source of snapshot.files) {
|
|
426
|
+
const filename = source.filename;
|
|
427
|
+
if (!TEXT_EXT.has(path.extname(filename).toLowerCase())) {
|
|
428
|
+
files.push({ filename, included: false, reason: 'non-text policy exclusion', size: source.size,
|
|
429
|
+
sourceGit: { ...source.sourceGit, captureMethod: 'public-bare-git-v1', revisionKind: 'git-commit',
|
|
430
|
+
observedAt: observation.observedAt, observedRowsSha256,
|
|
431
|
+
sourceObservationSha256: observation.observationSha256 } });
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
let text;
|
|
435
|
+
try { text = new TextDecoder('utf-8', { fatal: true }).decode(source.body); }
|
|
436
|
+
catch { throw new Error(`gist ${stub.id}/${filename} is not valid UTF-8 text`); }
|
|
437
|
+
files.push({ filename, included: true, sha256: sha256(source.body), bytes: source.size,
|
|
438
|
+
body: text, sourceGit: { ...source.sourceGit, captureMethod: 'public-bare-git-v1', revisionKind: 'git-commit',
|
|
439
|
+
observedAt: observation.observedAt, observedRowsSha256,
|
|
440
|
+
sourceObservationSha256: observation.observationSha256 } });
|
|
441
|
+
}
|
|
442
|
+
if (files.length !== snapshot.treeFileCount) throw new Error(`gist ${stub.id} Git inventory changed during capture`);
|
|
443
|
+
gists[stub.id] = { gistId: stub.id, versionSha: snapshot.headSha, updatedAt: stub.updated_at,
|
|
444
|
+
ingestedAt: now(), complete: true, files };
|
|
445
|
+
fetched.push(stub.id);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
355
448
|
const cached = cache?.gists?.[stub.id];
|
|
356
449
|
const verdict = cached ? reusableCachedGist(cached, stub) : 'miss';
|
|
357
450
|
if (verdict === 'tampered') {
|
|
@@ -386,14 +479,31 @@ export async function captureGistSources({ observation, cache = null, fetchDetai
|
|
|
386
479
|
complete: files.length === Object.keys(full.files || {}).length, files };
|
|
387
480
|
fetched.push(stub.id);
|
|
388
481
|
}
|
|
389
|
-
|
|
482
|
+
const captured = {
|
|
390
483
|
owner: observation.owner,
|
|
484
|
+
observedRowsSha256,
|
|
391
485
|
observedAt: observation.observedAt,
|
|
392
486
|
sourceObservationSha256: observation.observationSha256,
|
|
393
487
|
generatedAt: now(),
|
|
394
488
|
gists,
|
|
395
489
|
reuseEvidence: { reused, fetched },
|
|
396
490
|
};
|
|
491
|
+
VERIFIED_CAPTURE_SETS.set(captured, {
|
|
492
|
+
fingerprint: digest(captured),
|
|
493
|
+
owner: observation.owner,
|
|
494
|
+
observedRowsSha256,
|
|
495
|
+
sourceObservationSha256: observation.observationSha256,
|
|
496
|
+
});
|
|
497
|
+
return captured;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function isCurrentVerifiedCapture(captured, observation) {
|
|
501
|
+
const proof = captured && typeof captured === 'object' ? VERIFIED_CAPTURE_SETS.get(captured) : null;
|
|
502
|
+
return Boolean(proof && proof.owner === observation?.owner
|
|
503
|
+
&& proof.observedRowsSha256 === digest({ owner: observation?.owner,
|
|
504
|
+
observedAt: observation?.observedAt, rows: observation?.gists?.rows })
|
|
505
|
+
&& proof.sourceObservationSha256 === observation?.observationSha256
|
|
506
|
+
&& proof.fingerprint === digest(captured));
|
|
397
507
|
}
|
|
398
508
|
|
|
399
509
|
// ── stage 2: render ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -462,6 +572,25 @@ export function validateGistReceipt({ receipt, observation = null, passagesFile,
|
|
|
462
572
|
if (!validDate(row?.updatedAt) || !validDate(row?.ingestedAt)) {
|
|
463
573
|
throw new Error(`gist ${gistId} has an invalid updatedAt/ingestedAt timestamp`);
|
|
464
574
|
}
|
|
575
|
+
const sourceProofs = (row?.files || []).map((file) => file?.sourceGit).filter(Boolean);
|
|
576
|
+
if (sourceProofs.length && sourceProofs.length !== row.files.length) {
|
|
577
|
+
throw new Error(`gist ${gistId} mixes Git-bound and unbound file evidence`);
|
|
578
|
+
}
|
|
579
|
+
if (sourceProofs.length && sourceProofs.some((proof) => !HEX40.test(String(proof.headSha || ''))
|
|
580
|
+
|| !HEX40.test(String(proof.treeSha || '')) || !HEX40.test(String(proof.blobSha || ''))
|
|
581
|
+
|| proof.captureMethod !== 'public-bare-git-v1' || proof.revisionKind !== 'git-commit'
|
|
582
|
+
|| !validDate(proof.observedAt) || proof.sourceObservationSha256 !== receipt.sourceObservationSha256
|
|
583
|
+
|| !HEX64.test(String(proof.observedRowsSha256 || ''))
|
|
584
|
+
|| !Number.isSafeInteger(proof.treeFileCount) || proof.treeFileCount !== row.files.length
|
|
585
|
+
|| proof.observedTruncated !== false || proof.observedFileCount !== proof.treeFileCount
|
|
586
|
+
|| proof.headSha !== row.versionSha || proof.observed !== true
|
|
587
|
+
|| !HEX40.test(String(proof.observedRawRevisionSha || ''))
|
|
588
|
+
|| !['blob', 'commit'].includes(proof.observedRawRevisionKind)
|
|
589
|
+
|| proof.observedRawBlobSha !== proof.blobSha
|
|
590
|
+
|| sourceProofs.some((other) => other.treeSha !== proof.treeSha || other.headSha !== proof.headSha
|
|
591
|
+
|| other.observedRowsSha256 !== proof.observedRowsSha256))) {
|
|
592
|
+
throw new Error(`gist ${gistId} has an incomplete Git snapshot proof`);
|
|
593
|
+
}
|
|
465
594
|
for (const file of row?.files || []) {
|
|
466
595
|
if (!validateFilename(file?.filename)) throw new Error(`gist ${gistId} has an unsafe or missing filename`);
|
|
467
596
|
}
|
|
@@ -517,8 +646,9 @@ export async function buildGistAggregate({ observation, cache = null, outDir, ro
|
|
|
517
646
|
sourceMetadata: null, files: [], sourceReceipt: null, reuseEvidence: { reused: [], fetched: [] } };
|
|
518
647
|
}
|
|
519
648
|
|
|
520
|
-
const captured = await captureGistSources({
|
|
521
|
-
observation, cache, fetchDetail: transport.fetchDetail, fetchRaw: transport.fetchRaw,
|
|
649
|
+
const captured = isCurrentVerifiedCapture(cache, observation) ? cache : await captureGistSources({
|
|
650
|
+
observation, cache, fetchDetail: transport.fetchDetail, fetchRaw: transport.fetchRaw,
|
|
651
|
+
fetchGitSnapshot: transport.fetchGitSnapshot, signal, now,
|
|
522
652
|
});
|
|
523
653
|
const { passageBytes, metadata, gistRecords } = renderGistPassages({ captured, generatedAt: captured.generatedAt });
|
|
524
654
|
const includedFileCount = Object.values(captured.gists)
|
package/scripts/ingest-gists.mjs
CHANGED
|
@@ -194,10 +194,13 @@ async function main() {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
const observedAt = new Date().toISOString();
|
|
197
|
-
const rows = gists.map((g) => ({ id: g.id, updated_at: g.updated_at,
|
|
197
|
+
const rows = gists.map((g) => ({ id: g.id, updated_at: g.updated_at, html_url: g.html_url,
|
|
198
|
+
truncated: g.truncated, files: g.files }));
|
|
198
199
|
const observation = {
|
|
199
200
|
owner: OWNER, observedAt,
|
|
200
|
-
|
|
201
|
+
// Bind the exact listed file inventory and its truncation indicator. A timestamp-only digest
|
|
202
|
+
// could otherwise reuse a capture after the API changes a file's pinned raw blob or completeness.
|
|
203
|
+
observationSha256: digest({ owner: OWNER, rows }),
|
|
201
204
|
gists: { rows },
|
|
202
205
|
};
|
|
203
206
|
const now = () => observedAt;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// ADR-089: describe installed identities without confusing a validator pin with search-engine proof.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { readInstalledRuntime, isCorpusReleaseTag } from '../kb/corpus-release-identity.mjs';
|
|
6
|
+
import { cmpVersion } from './stack-sync.mjs';
|
|
7
|
+
import { isRuntimeFile } from './approved-runtime.mjs';
|
|
8
|
+
|
|
9
|
+
const SEARCH_FILES = ['forge-mcp-all.mjs', 'forge-ask-all.mjs', 'forge-rerank.mjs', 'card-lane.mjs'];
|
|
10
|
+
// Keep the doctor probe answerable and bounded. A health probe must exercise a real indexed
|
|
11
|
+
// source/citation path; it must not turn an unclear broad question into an all-corpus rerank.
|
|
12
|
+
export const DOCTOR_SMOKE_QUERY = 'What package name is declared in the RuvNet Brain KB package manifest?';
|
|
13
|
+
export function doctorSmokeArgs(cacheDir) {
|
|
14
|
+
return ['forge-ask-all.mjs', '--dir', cacheDir, '--q', DOCTOR_SMOKE_QUERY,
|
|
15
|
+
'--repos', 'ruvnet-brain', '--k', '3', '--pool', '8', '--bounded'];
|
|
16
|
+
}
|
|
17
|
+
const version = value => typeof value === 'string' && /^v?\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(value)
|
|
18
|
+
? value.replace(/^v/, '') : null;
|
|
19
|
+
|
|
20
|
+
export function inspectInstalledBrain(kbDir, packageVersion) {
|
|
21
|
+
const result = { healthy: false, packageVersion, searchVersion: null, corpusTag: null,
|
|
22
|
+
validatorVersion: null, searchFiles: {}, issues: [] };
|
|
23
|
+
let source;
|
|
24
|
+
try { source = JSON.parse(fs.readFileSync(path.join(kbDir, 'SOURCE.json'), 'utf8')); }
|
|
25
|
+
catch { result.issues.push('SOURCE.json is missing or unreadable'); }
|
|
26
|
+
result.searchVersion = version(source?.brainVersion) || version(source?.releaseTag);
|
|
27
|
+
result.corpusTag = isCorpusReleaseTag(source?.corpusReleaseTag) ? source.corpusReleaseTag : null;
|
|
28
|
+
if (!result.searchVersion) result.issues.push('the search engine version is unverified');
|
|
29
|
+
if (version(source?.brainVersion) && version(source?.releaseTag)
|
|
30
|
+
&& version(source.brainVersion) !== version(source.releaseTag)) {
|
|
31
|
+
result.issues.push('SOURCE.json runtime version and release tag disagree');
|
|
32
|
+
}
|
|
33
|
+
const pin = readInstalledRuntime(kbDir);
|
|
34
|
+
result.validatorVersion = pin.brainVersion;
|
|
35
|
+
if (!pin.ok) result.issues.push(pin.reason);
|
|
36
|
+
if (pin.ok && result.searchVersion && version(pin.brainVersion) !== result.searchVersion) {
|
|
37
|
+
result.issues.push(`validator runtime ${pin.brainVersion} and search engine ${result.searchVersion} differ`);
|
|
38
|
+
}
|
|
39
|
+
const expected = version(packageVersion);
|
|
40
|
+
if (expected && result.searchVersion && cmpVersion(result.searchVersion, expected) < 0) {
|
|
41
|
+
result.issues.push(`search engine ${result.searchVersion} is behind package ${packageVersion}`);
|
|
42
|
+
}
|
|
43
|
+
for (const file of SEARCH_FILES) {
|
|
44
|
+
try {
|
|
45
|
+
const bytes = fs.readFileSync(path.join(kbDir, file));
|
|
46
|
+
result.searchFiles[file] = { bytes: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') };
|
|
47
|
+
if (!bytes.length) result.issues.push(`search executable ${file} is empty`);
|
|
48
|
+
} catch { result.issues.push(`search executable ${file} is missing`); }
|
|
49
|
+
}
|
|
50
|
+
// The installed archive manifest lets doctor detect code drift even if SOURCE's version stayed
|
|
51
|
+
// unchanged. This is a consistency check, not independent authentication of a local manifest.
|
|
52
|
+
try {
|
|
53
|
+
const archive = JSON.parse(fs.readFileSync(path.join(kbDir, 'ARCHIVE-MANIFEST.json'), 'utf8'));
|
|
54
|
+
if (archive.kind !== 'ruvnet-brain-archive-manifest' || !Array.isArray(archive.files)) {
|
|
55
|
+
throw new Error('invalid archive manifest shape');
|
|
56
|
+
}
|
|
57
|
+
if (version(archive.version) !== result.searchVersion) {
|
|
58
|
+
result.issues.push('archive manifest and search engine versions differ');
|
|
59
|
+
}
|
|
60
|
+
const runtimeFiles = new Set([...SEARCH_FILES, ...archive.files.filter(row => isRuntimeFile(row?.path)).map(row => row.path)]);
|
|
61
|
+
for (const file of runtimeFiles) {
|
|
62
|
+
if (typeof file !== 'string' || path.isAbsolute(file) || file.includes('\\')
|
|
63
|
+
|| file.split('/').some(part => !part || part === '.' || part === '..')) {
|
|
64
|
+
result.issues.push('archive manifest contains an unsafe runtime path');
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const rows = archive.files.filter(row => row?.path === file);
|
|
68
|
+
let actual = result.searchFiles[file];
|
|
69
|
+
if (!actual) {
|
|
70
|
+
try {
|
|
71
|
+
const bytes = fs.readFileSync(path.join(kbDir, file));
|
|
72
|
+
actual = { bytes: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') };
|
|
73
|
+
result.searchFiles[file] = actual;
|
|
74
|
+
} catch { result.issues.push(`runtime file ${file} is missing`); }
|
|
75
|
+
}
|
|
76
|
+
if (rows.length !== 1 || !/^[a-f0-9]{64}$/.test(rows[0]?.sha256 || '')
|
|
77
|
+
|| !Number.isSafeInteger(rows[0]?.bytes) || rows[0].bytes < 0) {
|
|
78
|
+
result.issues.push(`archive manifest has no unique valid identity for ${file}`);
|
|
79
|
+
} else if (actual && (actual.sha256 !== rows[0].sha256 || actual.bytes !== rows[0].bytes)) {
|
|
80
|
+
result.issues.push(`search executable ${file} differs from the installed archive manifest`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
result.issues.push(`installed archive manifest is missing or unreadable: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
// These hashes and the local manifest are NOT independent release approval receipts.
|
|
87
|
+
result.healthy = result.issues.length === 0;
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function classifySmokeEvidence(verification, answer) {
|
|
92
|
+
if (!verification?.grounded) return { usable: false, reason: verification?.reason || 'citation-not-verified' };
|
|
93
|
+
const weak = /EVIDENCE:\s*(?:INSUFFICIENT_EVIDENCE|THIN)\b/i.test(answer);
|
|
94
|
+
if (weak) return { usable: false, reason: 'retrieval-evidence-insufficient' };
|
|
95
|
+
if (/BUILT\/SHIPPED CLAIM:\s*NOT PROVEN/i.test(answer)) {
|
|
96
|
+
return { usable: false, reason: 'required-implementation-unproven' };
|
|
97
|
+
}
|
|
98
|
+
return { usable: true, reason: null };
|
|
99
|
+
}
|
|
@@ -729,6 +729,50 @@ export function reconcileNightly({ choice, schedule }) {
|
|
|
729
729
|
};
|
|
730
730
|
}
|
|
731
731
|
|
|
732
|
+
// The one thing the console never showed: WHICH repositories are in the brain, by name. It has
|
|
733
|
+
// always carried the count (206) and never the inventory — the owner asked for this list more than
|
|
734
|
+
// a dozen times. RVF-GENERATIONS.json is the manifest that matches disk exactly (verified 206 = 206);
|
|
735
|
+
// SOURCE.json is the provenance subset (104) that carries the upstream URL. Join on store name,
|
|
736
|
+
// never restate either. Ranked by build recency — never alphabetical (relevance-ordering rule).
|
|
737
|
+
function gatherInventory() {
|
|
738
|
+
const generations = readJSON(path.join(INSTALLED_KB, 'RVF-GENERATIONS.json'));
|
|
739
|
+
const source = readJSON(path.join(INSTALLED_KB, 'SOURCE.json'));
|
|
740
|
+
const stores = generations?.stores && typeof generations.stores === 'object' ? generations.stores : null;
|
|
741
|
+
if (!stores) return { available: false, reason: 'RVF-GENERATIONS.json not readable in the installed brain', count: 0, stores: [] };
|
|
742
|
+
// SOURCE.json has carried `stores` as an array in some generations and a name-keyed object in
|
|
743
|
+
// others; accept both rather than silently joining nothing (measured 2026-09-16: object form,
|
|
744
|
+
// and the array-only read reported withProvenance=0 for a brain with 104 provenance rows).
|
|
745
|
+
const provenance = new Map();
|
|
746
|
+
const sourceRows = Array.isArray(source?.stores) ? source.stores
|
|
747
|
+
: source?.stores && typeof source.stores === 'object'
|
|
748
|
+
? Object.entries(source.stores).map(([name, row]) => ({ name, ...row })) : [];
|
|
749
|
+
for (const row of sourceRows) {
|
|
750
|
+
if (row?.name) provenance.set(String(row.name).toLowerCase(), row);
|
|
751
|
+
}
|
|
752
|
+
const rows = Object.entries(stores).map(([name, gen]) => {
|
|
753
|
+
const prov = provenance.get(name.toLowerCase());
|
|
754
|
+
return {
|
|
755
|
+
name,
|
|
756
|
+
sourceRepo: prov?.sourceRepo || null,
|
|
757
|
+
sourceCommit: gen?.sourceCommit ? String(gen.sourceCommit).slice(0, 7) : null,
|
|
758
|
+
builtUtc: gen?.builtUtc || null,
|
|
759
|
+
bytes: Number(gen?.bytes) || 0,
|
|
760
|
+
model: gen?.model || null,
|
|
761
|
+
private: prov?.updateManaged === false,
|
|
762
|
+
};
|
|
763
|
+
}).sort((a, b) => String(b.builtUtc || '').localeCompare(String(a.builtUtc || '')) || a.name.localeCompare(b.name));
|
|
764
|
+
return {
|
|
765
|
+
available: true,
|
|
766
|
+
count: rows.length,
|
|
767
|
+
withProvenance: rows.filter((r) => r.sourceRepo).length,
|
|
768
|
+
privateCount: rows.filter((r) => r.private).length,
|
|
769
|
+
brainVersion: generations.brainVersion || null,
|
|
770
|
+
releaseTag: generations.releaseTag || null,
|
|
771
|
+
totalBytes: rows.reduce((sum, r) => sum + r.bytes, 0),
|
|
772
|
+
stores: rows,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
732
776
|
function gatherConfig() {
|
|
733
777
|
const cfg = readJSON(CONFIG_PATH) || {};
|
|
734
778
|
const credential = openRouterCredentialStatus({ cwd: process.cwd() });
|
|
@@ -2268,7 +2312,7 @@ function gatherState(cwd, { fleet = true } = {}) {
|
|
|
2268
2312
|
generatedAt: new Date().toISOString(),
|
|
2269
2313
|
preStateHash,
|
|
2270
2314
|
host: { user: os.userInfo().username, platform: process.platform, node: process.version, npmPrefix: NPM_PREFIX.replace(SYSTEM_HOME, '~'), brainVersion: brainVersionOnDisk() },
|
|
2271
|
-
sections: { wiring, memory, savings, config, userSettings, brainPower, gates, recommendations },
|
|
2315
|
+
sections: { wiring, memory, savings, config, userSettings, brainPower, gates, recommendations, inventory: gatherInventory() },
|
|
2272
2316
|
};
|
|
2273
2317
|
// Cache the last good state so repeat page-loads paint instantly, same as the stack audit does.
|
|
2274
2318
|
// TOKEN is per-server-run and must never touch disk — ?fast=1 splices the live one back in.
|
|
@@ -36,6 +36,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
36
36
|
import { digest, fileIdentity } from './coverage-integrity.mjs';
|
|
37
37
|
import { isPrivate, loadPrivateFence, loadPrivateSlugs, shouldFenceL2 } from './private-fence.mjs';
|
|
38
38
|
import { promoteArtifactSet } from '../kb/incremental-refresh.mjs';
|
|
39
|
+
import { isCapabilityOnly } from '../kb/capability-only.mjs';
|
|
39
40
|
|
|
40
41
|
const DEFAULT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
41
42
|
const HEX40 = /^[a-f0-9]{40}$/;
|
|
@@ -299,7 +300,7 @@ export function materializePublicInputs({ builderRoot = DEFAULT_ROOT, policy = {
|
|
|
299
300
|
try {
|
|
300
301
|
for (const repo of allRepos) {
|
|
301
302
|
const src = path.join(kb, `${repo}-primer.md`);
|
|
302
|
-
if (isPrivate(privateSet, repo)) {
|
|
303
|
+
if (isPrivate(privateSet, repo) || isCapabilityOnly(repo)) {
|
|
303
304
|
excluded.primers.push({ repo, ...excludedIdentity(src) });
|
|
304
305
|
continue;
|
|
305
306
|
}
|
|
@@ -113,6 +113,18 @@ function exactNamedFile(root, name, label) {
|
|
|
113
113
|
return trustedFile(path.join(root, name), label);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
function validHistoricalGenerationLedger(ledger, names) {
|
|
117
|
+
const legacy = ledger?.schemaVersion === 1;
|
|
118
|
+
const runtime = ledger?.schemaVersion === 2
|
|
119
|
+
&& ledger?.kind === 'ruvnet-brain-runtime-generation-ledger'
|
|
120
|
+
&& HEX40.test(String(ledger?.sourceSnapshot || ''));
|
|
121
|
+
return (legacy || runtime)
|
|
122
|
+
&& typeof ledger?.brainVersion === 'string' && Boolean(ledger.brainVersion)
|
|
123
|
+
&& typeof ledger?.releaseTag === 'string' && Boolean(ledger.releaseTag)
|
|
124
|
+
&& names.length > 0
|
|
125
|
+
&& new Set(names.map((name) => name.toLowerCase())).size === names.length;
|
|
126
|
+
}
|
|
127
|
+
|
|
116
128
|
function validateCandidate({ root, bundleFile, packageFile }) {
|
|
117
129
|
const coverageFile = trustedFile(path.join(root, 'COVERAGE.json'), 'candidate release coverage');
|
|
118
130
|
const coverageBytes = fs.readFileSync(coverageFile);
|
|
@@ -176,9 +188,7 @@ function retrospectiveBaselineFromTree({ extractedRoot, bundleFile, expectedTag,
|
|
|
176
188
|
const root = path.dirname(ledgerFile);
|
|
177
189
|
const { value: ledger } = readJson(ledgerFile, 'historical baseline generation ledger');
|
|
178
190
|
const names = Object.keys(ledger?.stores || {}).sort();
|
|
179
|
-
if (ledger
|
|
180
|
-
|| typeof ledger.releaseTag !== 'string' || !ledger.releaseTag || !names.length
|
|
181
|
-
|| new Set(names.map((name) => name.toLowerCase())).size !== names.length) {
|
|
191
|
+
if (!validHistoricalGenerationLedger(ledger, names)) {
|
|
182
192
|
fail('historical baseline generation ledger is malformed');
|
|
183
193
|
}
|
|
184
194
|
const archive = namedIdentity(bundleFile);
|
|
@@ -245,9 +255,7 @@ function observedBaselineFromTree({ extractedRoot, bundleFile, expectedTag, expe
|
|
|
245
255
|
const root = path.dirname(ledgerFile);
|
|
246
256
|
const { value: ledger } = readJson(ledgerFile, 'historical baseline generation ledger');
|
|
247
257
|
const ledgerNames = Object.keys(ledger?.stores || {}).sort();
|
|
248
|
-
if (ledger
|
|
249
|
-
|| typeof ledger.releaseTag !== 'string' || !ledger.releaseTag || !ledgerNames.length
|
|
250
|
-
|| new Set(ledgerNames.map((name) => name.toLowerCase())).size !== ledgerNames.length) {
|
|
258
|
+
if (!validHistoricalGenerationLedger(ledger, ledgerNames)) {
|
|
251
259
|
fail('historical baseline generation ledger is malformed');
|
|
252
260
|
}
|
|
253
261
|
const archive = namedIdentity(bundleFile);
|