ruvnet-brain 4.3.26 → 4.3.28
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/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/candidate-host-evidence.mjs +44 -16
- 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/host-install-matrix.mjs +54 -11
- package/scripts/ingest-gists.mjs +5 -2
- package/scripts/installed-brain-health.mjs +99 -0
- package/scripts/prepublication-evidence.mjs +8 -0
- package/scripts/public-inputs.mjs +2 -1
- package/scripts/public-verification-inputs.mjs +14 -6
- package/scripts/publication-receipt.mjs +71 -30
- package/scripts/qe/ux-suite.mjs +17 -5
- package/scripts/refresh-capability-only-store.mjs +143 -0
- package/scripts/release-qualification-contract.mjs +11 -1
- 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/staged-host-verifier.mjs +2 -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)
|
|
@@ -32,6 +32,11 @@ import { runRetrievalCanaries, validateRetrievalCanaryReceipt, resolveInstalledC
|
|
|
32
32
|
|
|
33
33
|
/** The three host shapes a release must survive. ONE name each, for every consumer. */
|
|
34
34
|
export const HOST_MODES = Object.freeze(['claude', 'codex', 'dual']);
|
|
35
|
+
export const SELF_STORE_PROOF_QUERY = 'repo:ruvnet-brain What is the RuvNet Brain release evidence workflow?';
|
|
36
|
+
export const SELF_STORE_PROOF_K = 1;
|
|
37
|
+
export const RELEASE_SEARCH_QUERY = 'repo:ruvnet-brain How does RuvNet Brain prove a public release artifact?';
|
|
38
|
+
export const HOST_WARMUP_TIMEOUT_MS = 300_000;
|
|
39
|
+
export const RELEASE_SEARCH_DEADLINE_MS = 30_000;
|
|
35
40
|
|
|
36
41
|
/** Which CLIs each mode is allowed to see. A codex-only box genuinely has no `claude`. */
|
|
37
42
|
export const MODE_HOSTS = Object.freeze({
|
|
@@ -204,6 +209,7 @@ export async function runHostMatrixAsync({
|
|
|
204
209
|
verifyGrounding = verifyInstalledGrounding,
|
|
205
210
|
resolveMcpServer = resolveInstalledMcpServer,
|
|
206
211
|
retrieval,
|
|
212
|
+
sequentialSearches = false,
|
|
207
213
|
}) {
|
|
208
214
|
const spec = VARIANTS[variant];
|
|
209
215
|
if (!spec) throw new Error(`unknown host-matrix variant: ${variant}`);
|
|
@@ -251,32 +257,57 @@ export async function runHostMatrixAsync({
|
|
|
251
257
|
|
|
252
258
|
const prewarmContext = contexts[0];
|
|
253
259
|
const prewarmReader = path.join(prewarmContext.env.RUVNET_BRAIN_KB, 'forge-ask-all.mjs');
|
|
260
|
+
// This query only primes the shared model cache. Keep it in the Brain's own bounded store so
|
|
261
|
+
// Mac/Windows/Linux runner speed does not determine whether the later real host searches run.
|
|
262
|
+
// The following MCP matrix remains the measured, source-grounded candidate acceptance.
|
|
254
263
|
const prewarm = await runCommand(process.execPath, [prewarmReader, '--dir', prewarmContext.env.RUVNET_BRAIN_KB,
|
|
255
|
-
'--q', '
|
|
264
|
+
'--q', RELEASE_SEARCH_QUERY, '--k', '1', '--pool', '8',
|
|
265
|
+
'--repos', 'ruvnet-brain', '--bounded'], {
|
|
256
266
|
cwd: prewarmContext.env.RUVNET_BRAIN_KB, env: prewarmContext.env, timeout: 300_000,
|
|
257
267
|
});
|
|
258
268
|
if (prewarm.error || prewarm.status !== 0) {
|
|
259
|
-
|
|
269
|
+
const detail = String(prewarm.stderr || prewarm.stdout || '').trim().split('\n').slice(-8).join(' | ').slice(-1600);
|
|
270
|
+
return { verdict: 'FAIL', fixtures: {}, error: `shared model prewarm failed (${processDiagnostic(prewarm)})${detail ? `; ${detail}` : ''}` };
|
|
260
271
|
}
|
|
261
272
|
const prewarmGrounding = await verifyGrounding(String(prewarm.stdout || ''), prewarmContext.env.RUVNET_BRAIN_KB);
|
|
262
273
|
if (!prewarmGrounding?.grounded) {
|
|
263
274
|
return { verdict: 'FAIL', fixtures: {}, error: 'shared model prewarm returned no grounded source receipt' };
|
|
264
275
|
}
|
|
265
276
|
|
|
266
|
-
const
|
|
277
|
+
const searchOne = async (context) => {
|
|
267
278
|
let session;
|
|
268
279
|
try {
|
|
269
280
|
const serverPath = resolveMcpServer(context);
|
|
270
281
|
// One installed worker per host: model/store state survives smoke and every sealed case.
|
|
271
282
|
session = runMcpSearch === runInstalledMcpSearch ? createInstalledMcpSession({ serverPath, env: context.env }) : null;
|
|
272
283
|
const searchMcp = session ? (args) => session.search(args) : runMcpSearch;
|
|
273
|
-
|
|
284
|
+
// The installed shell warms its in-process models and stores asynchronously from MCP
|
|
285
|
+
// initialize. Candidate qualification must give that same worker the same cited readiness
|
|
286
|
+
// probe used by the public verifier before timing the first user search; a separate CLI
|
|
287
|
+
// prewarm only primes disk cache and cannot warm this worker's process memory.
|
|
288
|
+
const warmupResult = await searchMcp({ mode: context.mode, serverPath, env: context.env,
|
|
289
|
+
query: SELF_STORE_PROOF_QUERY, k: SELF_STORE_PROOF_K, timeoutMs: HOST_WARMUP_TIMEOUT_MS });
|
|
290
|
+
const warmupOutput = `${warmupResult.stdout || ''}${warmupResult.stderr || ''}`;
|
|
291
|
+
if (warmupResult.error || warmupResult.status !== 0) {
|
|
292
|
+
return { context, processResult: warmupResult,
|
|
293
|
+
error: `MCP warmup failed for ${context.mode} (${processDiagnostic(warmupResult)}): ${warmupOutput.slice(-2000)}` };
|
|
294
|
+
}
|
|
295
|
+
const warmupGrounding = await verifyGrounding(warmupOutput, context.env.RUVNET_BRAIN_KB);
|
|
296
|
+
if (!warmupGrounding?.grounded || warmupGrounding.receipt?.repo !== 'ruvnet-brain') {
|
|
297
|
+
return { context, processResult: warmupResult, warmupMs: warmupResult.broadMs,
|
|
298
|
+
error: `MCP self-store warmup grounding unproven for ${context.mode}` };
|
|
299
|
+
}
|
|
300
|
+
const processResult = await searchMcp({ mode: context.mode, serverPath, env: context.env,
|
|
301
|
+
query: RELEASE_SEARCH_QUERY, k: 5,
|
|
302
|
+
timeoutMs: RELEASE_SEARCH_DEADLINE_MS });
|
|
274
303
|
const output = `${processResult.stdout || ''}${processResult.stderr || ''}`;
|
|
275
304
|
if (processResult.error || processResult.status !== 0) {
|
|
276
|
-
return { context, processResult,
|
|
305
|
+
return { context, processResult, warmupMs: warmupResult.broadMs, warmupGrounding,
|
|
306
|
+
error: `MCP search failed for ${context.mode} (${processDiagnostic(processResult)}): ${output.slice(-2000)}` };
|
|
277
307
|
}
|
|
278
308
|
const grounding = await verifyGrounding(output, context.env.RUVNET_BRAIN_KB);
|
|
279
|
-
if (!grounding?.grounded) return { context, processResult,
|
|
309
|
+
if (!grounding?.grounded) return { context, processResult, warmupMs: warmupResult.broadMs,
|
|
310
|
+
warmupGrounding, error: `MCP search grounding unproven for ${context.mode}` };
|
|
280
311
|
let receipt;
|
|
281
312
|
if (retrieval) {
|
|
282
313
|
receipt = await runRetrievalCanaries({ ...retrieval,
|
|
@@ -288,17 +319,29 @@ export async function runHostMatrixAsync({
|
|
|
288
319
|
citationResolver: (matched, expected) => resolveInstalledCanaryCitation({ kbDir: context.env.RUVNET_BRAIN_KB, matched, expected }),
|
|
289
320
|
});
|
|
290
321
|
try { validateRetrievalCanaryReceipt(receipt, { plan: retrieval.plan }); }
|
|
291
|
-
catch (error) { return { context, processResult,
|
|
322
|
+
catch (error) { return { context, processResult, warmupMs: warmupResult.broadMs,
|
|
323
|
+
warmupGrounding, grounding, retrieval: receipt, error: `${context.mode} canary rejected: ${error.message}` }; }
|
|
292
324
|
}
|
|
293
|
-
return { context, processResult,
|
|
325
|
+
return { context, processResult, warmupMs: warmupResult.broadMs, warmupGrounding,
|
|
326
|
+
grounding, ...(receipt ? { retrieval: receipt } : {}) };
|
|
294
327
|
} catch (error) {
|
|
295
328
|
return { context, processResult: { status: null, error }, error: `${context.mode} host search failed: ${error.message}` };
|
|
296
329
|
} finally { await session?.close(); }
|
|
297
|
-
}
|
|
298
|
-
const
|
|
330
|
+
};
|
|
331
|
+
const searches = [];
|
|
332
|
+
if (sequentialSearches) {
|
|
333
|
+
for (const context of contexts) searches.push(await searchOne(context));
|
|
334
|
+
} else {
|
|
335
|
+
searches.push(...await Promise.all(contexts.map(searchOne)));
|
|
336
|
+
}
|
|
337
|
+
const fixtures = Object.fromEntries(searches.map(({ context, processResult, warmupMs, warmupGrounding,
|
|
338
|
+
grounding, retrieval, error }) => [context.mode, {
|
|
299
339
|
status: error ? 'FAIL' : 'PASS',
|
|
300
340
|
version,
|
|
301
341
|
process: processIdentity(processResult),
|
|
342
|
+
warmupMs: Number.isFinite(warmupMs) ? warmupMs : null,
|
|
343
|
+
...(warmupGrounding?.receipt ? { warmupGrounding: warmupGrounding.receipt } : {}),
|
|
344
|
+
searchMs: Number.isFinite(processResult.broadMs) ? processResult.broadMs : null,
|
|
302
345
|
...(grounding?.receipt ? { grounding: grounding.receipt } : {}),
|
|
303
346
|
...(retrieval ? { retrieval } : {}),
|
|
304
347
|
...(error ? { error } : {}),
|
|
@@ -395,7 +438,7 @@ export function createInstalledMcpSession({ serverPath, env, timeout = 300_000,
|
|
|
395
438
|
pending.set(id, { resolve, reject });
|
|
396
439
|
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
|
|
397
440
|
});
|
|
398
|
-
const execute = async ({ query =
|
|
441
|
+
const execute = async ({ query = RELEASE_SEARCH_QUERY, k = 5,
|
|
399
442
|
timeoutMs = timeout } = {}) => {
|
|
400
443
|
if (terminalError) return { status: null, error: terminalError, signal: exitSignal, stdout: '', stderr };
|
|
401
444
|
stderr = '';
|
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
|
+
}
|
|
@@ -9,6 +9,7 @@ import { RELEASE_QUALIFICATION_POLICY } from './integration-evidence.mjs';
|
|
|
9
9
|
import { readCandidateRetrieval } from './staged-host-verifier.mjs';
|
|
10
10
|
import { validateRetrievalCanaryReceipt } from './retrieval-canary.mjs';
|
|
11
11
|
import { payloadIdFor } from './release-payload.mjs';
|
|
12
|
+
import { HOST_WARMUP_TIMEOUT_MS, RELEASE_SEARCH_DEADLINE_MS } from './host-install-matrix.mjs';
|
|
12
13
|
|
|
13
14
|
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
14
15
|
|
|
@@ -95,11 +96,18 @@ export function buildPrepublicationEvidence({
|
|
|
95
96
|
requireExactSet(host.value.leaves.map(({ name }) => name), ['claude-only', 'codex-only', 'dual-host'], 'candidate host leaves');
|
|
96
97
|
for (const leaf of host.value.leaves) {
|
|
97
98
|
const grounding = leaf.grounding;
|
|
99
|
+
const warmupGrounding = leaf.warmupGrounding;
|
|
98
100
|
const grounded = grounding && ['repo', 'path', 'file', 'storedPath']
|
|
99
101
|
.every((field) => typeof grounding[field] === 'string' && grounding[field].trim());
|
|
102
|
+
const warmupGrounded = warmupGrounding && ['repo', 'path', 'file', 'storedPath']
|
|
103
|
+
.every((field) => typeof warmupGrounding[field] === 'string' && warmupGrounding[field].trim());
|
|
104
|
+
const selfStoreWarmup = warmupGrounding?.repo === 'ruvnet-brain';
|
|
100
105
|
if (leaf.sha !== sha || leaf.payloadId !== payload.payloadId || leaf.status !== 'completed'
|
|
101
106
|
|| leaf.conclusion !== 'success' || leaf.verdict !== 'PASS'
|
|
102
107
|
|| leaf.functionalSearch !== true || leaf.searchExit !== 0 || !grounded
|
|
108
|
+
|| !Number.isFinite(leaf.warmupMs) || leaf.warmupMs < 0 || leaf.warmupMs > HOST_WARMUP_TIMEOUT_MS
|
|
109
|
+
|| !warmupGrounded || !selfStoreWarmup
|
|
110
|
+
|| !Number.isFinite(leaf.searchMs) || leaf.searchMs < 0 || leaf.searchMs > RELEASE_SEARCH_DEADLINE_MS
|
|
103
111
|
|| leaf.artifactSha256 !== host.value.artifactSha256) {
|
|
104
112
|
throw new Error(`candidate host leaf is not an exact PASS: ${leaf.name || '(missing)'}`);
|
|
105
113
|
}
|
|
@@ -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
|
}
|