ruvnet-brain 4.3.20 → 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.
- package/README.md +5 -5
- package/bin/install.mjs +383 -78
- package/console/app.js +141 -9
- package/console/index.html +51 -24
- package/console/scope.css +137 -0
- package/console/scope.html +144 -0
- package/console/scope.js +209 -0
- package/console/tips.html +1 -0
- package/kb/corpus-release-identity.mjs +239 -0
- package/kb/update-storage-transaction.mjs +20 -3
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/checkpoint.md +61 -0
- package/plugin/hooks/codex-hooks.json +64 -1
- package/plugin/hooks/hook-contracts.json +299 -6
- package/plugin/hooks/hooks.json +81 -1
- package/plugin/mcp/server.mjs +23 -0
- package/plugin/scripts/advocacy-catalog.mjs +245 -0
- package/plugin/scripts/advocacy-route.mjs +460 -0
- package/plugin/scripts/continuation-gate.mjs +25 -2
- package/plugin/scripts/continuation-objective.mjs +7 -1
- package/plugin/scripts/continuity-hook-policy.mjs +190 -15
- package/plugin/scripts/coverage-integrity.mjs +7 -0
- package/plugin/scripts/gates.mjs +113 -10
- package/plugin/scripts/grounding-turn-gate.mjs +167 -0
- package/plugin/scripts/grounding-turn-mark.mjs +91 -0
- package/plugin/scripts/hook-shim.mjs +14 -0
- package/plugin/scripts/host-shell-boundary.mjs +43 -0
- package/plugin/scripts/nightly-scheduler.mjs +37 -4
- package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
- package/plugin/scripts/project-progression-contract.mjs +16 -0
- package/plugin/scripts/project-progression-hook.mjs +3 -0
- package/plugin/scripts/project-progression-producer.mjs +252 -0
- package/plugin/scripts/project-progression-reader.mjs +271 -0
- package/plugin/scripts/project-progression-session-start.mjs +93 -16
- package/plugin/scripts/project-progression-sources.mjs +220 -0
- package/plugin/scripts/project-progression-store.mjs +106 -13
- package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
- package/plugin/scripts/session-snapshot-hook.mjs +115 -7
- package/plugin/scripts/session-start-budget.mjs +59 -0
- package/plugin/scripts/session-start-core.mjs +234 -457
- package/plugin/scripts/session-start-fsutil.mjs +61 -0
- package/plugin/scripts/session-start-health.mjs +64 -0
- package/plugin/scripts/session-start-hook-description.mjs +45 -0
- package/plugin/scripts/session-start-issue-alert.mjs +77 -0
- package/plugin/scripts/session-start-repo-identity.mjs +54 -0
- package/plugin/scripts/session-start-signals.mjs +73 -0
- package/plugin/scripts/session-start-trace.mjs +86 -0
- package/plugin/scripts/session-start-update-plane.mjs +104 -0
- package/plugin/scripts/unprompted-runtime.mjs +32 -2
- package/plugin/scripts/update-apply.mjs +2 -32
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
- package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
- package/scripts/adr-072-completion.mjs +1 -1
- package/scripts/agentdb-fleet-doctor.mjs +5 -1
- package/scripts/approved-runtime.mjs +197 -0
- package/scripts/brain-novice-50.mjs +16 -1
- package/scripts/brain-score.mjs +23 -5
- package/scripts/build-bundle.mjs +971 -530
- package/scripts/build-concepts.mjs +36 -116
- package/scripts/console-engine.test.mjs +8 -7
- package/scripts/console-runtime-identity.mjs +4 -0
- package/scripts/corpus-aggregates.mjs +94 -77
- package/scripts/corpus-candidate.mjs +475 -222
- package/scripts/corpus-next-seed.mjs +225 -0
- package/scripts/corpus-promotion.mjs +58 -0
- package/scripts/corpus-reconcile.mjs +411 -105
- package/scripts/doc-currency.mjs +16 -1
- package/scripts/dual-host-deliberation.mjs +25 -2
- package/scripts/dual-host-suggest.mjs +17 -1
- package/scripts/falsify.mjs +13 -3
- package/scripts/gist-receipts.mjs +482 -87
- package/scripts/github-health-watch.mjs +12 -2
- package/scripts/handoff-asset.mjs +34 -0
- package/scripts/hook-retirement-check.mjs +8 -1
- package/scripts/host-registry.mjs +1 -1
- package/scripts/ingest-gists.mjs +74 -101
- package/scripts/job-heartbeat.sh +77 -14
- package/scripts/learning-replay-execution.mjs +10 -4
- package/scripts/nightly-gists.sh +27 -13
- package/scripts/nightly-two-run-proof.mjs +1 -1
- package/scripts/nightly-watchdog.mjs +61 -4
- package/scripts/onboarding-console.mjs +319 -27
- package/scripts/oracle/produce-questions.mjs +293 -0
- package/scripts/oracle/producer-hosts.mjs +235 -0
- package/scripts/oracle/repo-recall.mjs +448 -0
- package/scripts/oracle/retrieval-accuracy.mjs +818 -0
- package/scripts/oracle/source-tree.mjs +165 -0
- package/scripts/oracle/source-units.mjs +391 -0
- package/scripts/oracle/spike-run.mjs +98 -0
- package/scripts/oracle/unit-inventory.mjs +141 -0
- package/scripts/oracle/unit-sampling.mjs +128 -0
- package/scripts/oracle/validate-labels.mjs +250 -0
- package/scripts/private-overlay.mjs +248 -0
- package/scripts/product-integrity-contract.mjs +1 -1
- package/scripts/proxy/claude-proxied.sh +6 -0
- package/scripts/proxy/proxy-revert.sh +5 -0
- package/scripts/proxy/proxy-up.sh +6 -0
- package/scripts/proxy/proxy-verify.mjs +4 -0
- package/scripts/public-inputs.mjs +409 -0
- package/scripts/public-verification-inputs.mjs +112 -26
- package/scripts/public-verification-lane.mjs +1 -1
- package/scripts/published-surface-probe.mjs +34 -4
- package/scripts/qe/card-lane-gate.mjs +16 -1
- package/scripts/qe/session-start-gate.mjs +16 -1
- package/scripts/rebuild-gists-from-receipts.mjs +58 -78
- package/scripts/record-lesson.mjs +4 -1
- package/scripts/rehearse-corpus-pipeline.mjs +994 -0
- package/scripts/release-abort-stale.mjs +5 -1
- package/scripts/release-authority.mjs +104 -12
- package/scripts/release-channel-kind.mjs +86 -0
- package/scripts/release-convergence-watchdog.mjs +7 -2
- package/scripts/release-projection.mjs +177 -72
- package/scripts/release-transaction-provider.mjs +23 -6
- package/scripts/release.mjs +252 -17
- package/scripts/retrieval-canary.mjs +87 -0
- package/scripts/rvf-index-audit.mjs +573 -13
- package/scripts/rvf-wire.mjs +269 -0
- package/scripts/seal-gist-receipt.mjs +65 -0
- package/scripts/selfcheck.mjs +42 -21
- package/scripts/source-coverage.mjs +253 -24
- package/scripts/status-honesty.mjs +25 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/sync-version.mjs +2 -0
- package/scripts/trismart.mjs +42 -0
- package/scripts/updater-manifest.mjs +162 -0
- package/scripts/verify-channels.mjs +17 -5
- package/scripts/wired-check.mjs +48 -10
- package/tri-smart-skill/QUICKSTART.md +37 -0
- package/tri-smart-skill/README.md +92 -0
- package/tri-smart-skill/install.cmd +14 -0
- package/tri-smart-skill/install.command +13 -0
- package/tri-smart-skill/install.mjs +51 -0
- package/tri-smart-skill/install.sh +9 -0
- package/tri-smart-skill/tri-smart/SKILL.md +90 -0
- package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
- package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
- package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
- package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
- package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
- package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
- package/scripts/corpus-seed-publish.mjs +0 -110
|
@@ -1,23 +1,47 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { spawnSync } from 'node:child_process';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
import { readRvfGenerations, sha256File } from './rvf-generation.mjs';
|
|
8
9
|
import { canonicalJson, coverageGenerationFor, digest, validateGistAggregateReceipt } from './coverage-integrity.mjs';
|
|
9
10
|
import { repositoryNames } from '../kb/card-lane.mjs';
|
|
11
|
+
import { rootNeverMaterialized, storeRoot } from '../kb/store-root.mjs';
|
|
10
12
|
|
|
11
13
|
export { canonicalJson, digest } from './coverage-integrity.mjs';
|
|
12
14
|
|
|
13
15
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
+
|
|
17
|
+
// SOURCE POLICY VERSION — sealed into every coverage as `policy.policyVersion` so a recorded
|
|
18
|
+
// measurement produced under an older eligibility policy is DETECTABLE (main --check names the gap)
|
|
19
|
+
// rather than silently compared row-for-row against a generator that classifies differently.
|
|
20
|
+
// 1 — forks and archives INELIGIBLE by flag; anonymous gist fallback capped at 10 pages;
|
|
21
|
+
// no-corpus exclusions active on a pushedAt match alone.
|
|
22
|
+
// 2 — ADR-086 Step 12 (2026-09-13). Archives eligible unconditionally. Forks dispositioned every run
|
|
23
|
+
// by the compare API pinned to both observed heads: ahead_by>0 → `fork:original-content`
|
|
24
|
+
// (ingestible, delta-only), ahead_by=0 → `fork:no-original-content` (recorded with the upstream
|
|
25
|
+
// identity). Gist fallback paginates to an empty page under a throwing ceiling. An active
|
|
26
|
+
// exclusion must carry source-file evidence bound to the observed head.
|
|
27
|
+
export const SOURCE_POLICY_VERSION = 2;
|
|
28
|
+
|
|
29
|
+
// Dispositions whose rows the corpus is expected to contain. `fork:original-content` is deliberately
|
|
30
|
+
// NOT spelled `eligible`: scripts/corpus-reconcile.mjs full-clones every `eligible` repository row,
|
|
31
|
+
// and a full clone of a fork ingests upstream authors' commits under rUv's name — the misattribution
|
|
32
|
+
// the #286 RC3 repo-attribution guard exists to stop. The delta-only build that consumes this
|
|
33
|
+
// disposition is corpus-reconcile's work; this layer records what it needs (`forkDelta`).
|
|
34
|
+
const INGESTIBLE_DISPOSITIONS = new Set(['eligible', 'fork:original-content']);
|
|
35
|
+
export function isIngestibleDisposition(disposition) { return INGESTIBLE_DISPOSITIONS.has(disposition); }
|
|
36
|
+
|
|
14
37
|
const REPO_QUERY = `query($login:String!,$cursor:String){
|
|
15
38
|
user(login:$login){
|
|
16
39
|
publicRepositories:repositories(privacy:PUBLIC){totalCount}
|
|
17
40
|
repositories(first:100,after:$cursor,privacy:PUBLIC,ownerAffiliations:OWNER,orderBy:{field:NAME,direction:ASC}){
|
|
18
41
|
pageInfo{hasNextPage endCursor}
|
|
19
42
|
nodes{databaseId name url description homepageUrl isFork isArchived isDisabled diskUsage updatedAt pushedAt
|
|
20
|
-
defaultBranchRef{name target{... on Commit{oid committedDate}}}
|
|
43
|
+
defaultBranchRef{name target{... on Commit{oid committedDate}}}
|
|
44
|
+
parent{nameWithOwner isPrivate defaultBranchRef{name target{... on Commit{oid}}}}}
|
|
21
45
|
}
|
|
22
46
|
}
|
|
23
47
|
}`;
|
|
@@ -28,6 +52,34 @@ function runGh(args) {
|
|
|
28
52
|
return result.stdout;
|
|
29
53
|
}
|
|
30
54
|
|
|
55
|
+
// A fork's disposition depends on how many commits it carries that its upstream does not. The
|
|
56
|
+
// compare is pinned to the two heads THIS observation recorded (never to branch names, which move),
|
|
57
|
+
// and is run against the upstream repository in GitHub's documented cross-fork form
|
|
58
|
+
// `BASE...FORK_OWNER:HEAD` (verified live 2026-09-13: ahead_by/behind_by/status/merge_base_commit).
|
|
59
|
+
// Any failure here — 403, 404, an upstream GitHub cannot name — is required content that is
|
|
60
|
+
// inaccessible, and it fails the observation rather than skipping the fork.
|
|
61
|
+
function observeForkDelta(owner, node, gh) {
|
|
62
|
+
const forkHeadSha = node.defaultBranchRef?.target?.oid;
|
|
63
|
+
const upstream = node.parent?.nameWithOwner;
|
|
64
|
+
if (!upstream) throw new Error(`fork ${node.name} has no upstream parent GitHub can name — its original-content delta cannot be established`);
|
|
65
|
+
const upstreamHeadSha = node.parent.defaultBranchRef?.target?.oid;
|
|
66
|
+
if (!upstreamHeadSha) throw new Error(`fork ${node.name}: upstream ${upstream} has no default-branch head to compare against`);
|
|
67
|
+
let compare;
|
|
68
|
+
try {
|
|
69
|
+
compare = JSON.parse(gh(['api', `repos/${upstream}/compare/${upstreamHeadSha}...${owner}:${forkHeadSha}`]));
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new Error(`fork ${node.name}: compare against upstream ${upstream} failed — ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
if (!Number.isInteger(compare?.ahead_by) || !Number.isInteger(compare?.behind_by)) {
|
|
74
|
+
throw new Error(`fork ${node.name}: compare against upstream ${upstream} returned no ahead_by/behind_by`);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
upstream, upstreamDefaultBranch: node.parent.defaultBranchRef.name, upstreamHeadSha, forkHeadSha,
|
|
78
|
+
mergeBaseSha: compare.merge_base_commit?.sha || null,
|
|
79
|
+
aheadBy: compare.ahead_by, behindBy: compare.behind_by, status: compare.status || null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
31
83
|
export function observeRepositories(owner, { gh = runGh } = {}) {
|
|
32
84
|
const rows = [];
|
|
33
85
|
const pages = [];
|
|
@@ -45,7 +97,10 @@ export function observeRepositories(owner, { gh = runGh } = {}) {
|
|
|
45
97
|
} while (cursor);
|
|
46
98
|
const expected = rows.length ? Number(JSON.parse(gh(['api', `users/${owner}`])).public_repos) : 0;
|
|
47
99
|
if (rows.length !== expected) throw new Error(`repository enumeration incomplete: ${rows.length}/${expected}`);
|
|
48
|
-
|
|
100
|
+
// A fork with no head has nothing to compare; classifyRepository records it as `empty`.
|
|
101
|
+
const withDeltas = rows.map((node) => (node?.isFork && node.defaultBranchRef?.target?.oid
|
|
102
|
+
? { ...node, forkDelta: observeForkDelta(owner, node, gh) } : node));
|
|
103
|
+
return { rows: withDeltas, expected, pages };
|
|
49
104
|
}
|
|
50
105
|
|
|
51
106
|
export function observeExternalRepositories(sources, { gh = runGh } = {}) {
|
|
@@ -128,6 +183,13 @@ export function canonicalRepositoryRows(rows = []) {
|
|
|
128
183
|
homepageUrl: repo?.homepageUrl ?? null,
|
|
129
184
|
isFork: repo?.isFork === true,
|
|
130
185
|
...(repo?.upstreamIsFork === undefined ? {} : { upstreamIsFork: repo.upstreamIsFork === true }),
|
|
186
|
+
// Sealed into the source identity: the delta baseline (both heads, ahead/behind) IS what was observed.
|
|
187
|
+
...(repo?.forkDelta === undefined ? {} : { forkDelta: {
|
|
188
|
+
upstream: repo.forkDelta.upstream, upstreamDefaultBranch: repo.forkDelta.upstreamDefaultBranch ?? null,
|
|
189
|
+
upstreamHeadSha: repo.forkDelta.upstreamHeadSha, forkHeadSha: repo.forkDelta.forkHeadSha,
|
|
190
|
+
mergeBaseSha: repo.forkDelta.mergeBaseSha ?? null,
|
|
191
|
+
aheadBy: repo.forkDelta.aheadBy, behindBy: repo.forkDelta.behindBy, status: repo.forkDelta.status ?? null,
|
|
192
|
+
} }),
|
|
131
193
|
isArchived: repo?.isArchived === true,
|
|
132
194
|
isDisabled: repo?.isDisabled === true,
|
|
133
195
|
diskUsage: repo?.diskUsage,
|
|
@@ -188,11 +250,64 @@ export function gistVersion(gist) {
|
|
|
188
250
|
return versions.length && new Set(versions).size === 1 ? versions[0] : null;
|
|
189
251
|
}
|
|
190
252
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
253
|
+
// Test seam: same injection pattern as `gh = runGh` above, so the fallback's failure path is
|
|
254
|
+
// exercisable with a fake HTTP layer instead of the live API or even real loopback networking.
|
|
255
|
+
// Reads the API base fresh per call (not frozen at module load) for the same reason.
|
|
256
|
+
function runCurl(url) {
|
|
257
|
+
const result = spawnSync('curl', ['-sS', '--max-time', '30', '-H', 'accept: application/vnd.github+json',
|
|
258
|
+
'-H', 'user-agent: ruvnet-brain-source-coverage', url],
|
|
259
|
+
{ encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, timeout: 35_000 });
|
|
260
|
+
if (result.status !== 0) throw new Error(`unauthenticated gists list failed: ${(result.stderr || '').trim()}`);
|
|
261
|
+
return result.stdout;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Actions' GITHUB_TOKEN is a GitHub App token, and the gists list API is closed to those
|
|
265
|
+
// ("Resource not accessible by integration", HTTP 403 — confirmed live 2026-09-13, corpus-seed.yml's
|
|
266
|
+
// first-ever run). Public gists need no auth, so fall back to curl against the plain API on that
|
|
267
|
+
// specific failure, mirroring ingest-gists.mjs's already-working listGists/listGistsPublic split
|
|
268
|
+
// (kept synchronous here, matching this module's existing gh-injection style, rather than
|
|
269
|
+
// threading async through observeSourceUniverse/buildCoverage's whole synchronous call chain).
|
|
270
|
+
//
|
|
271
|
+
// ADR-086 Step 12 (A1): the former `page <= 10` bound was NOT a silent-omission bug — observeGists
|
|
272
|
+
// throws on rows.length !== public_gists, so beyond 1000 gists the run failed closed — but it was a
|
|
273
|
+
// ceiling the account could grow into. Pagination now runs to the ACTUAL end: the only terminator is
|
|
274
|
+
// an EMPTY page (a short page followed by more must not stop early), under a hard ceiling that
|
|
275
|
+
// THROWS rather than truncating. The rows.length === public_gists check below remains the one and
|
|
276
|
+
// only completeness proof; nothing here adds a second invariant.
|
|
277
|
+
const GIST_PAGE_CEILING = 100; // 10,000 gists at per_page=100
|
|
278
|
+
|
|
279
|
+
function listGistsUnauthenticated(owner, curl) {
|
|
280
|
+
const apiBase = process.env.RUVNET_GISTS_API || 'https://api.github.com';
|
|
281
|
+
const pages = [];
|
|
282
|
+
for (let page = 1; ; page++) {
|
|
283
|
+
if (page > GIST_PAGE_CEILING) {
|
|
284
|
+
throw new Error(`unauthenticated gists list exceeded ${GIST_PAGE_CEILING} pages without reaching an empty page`);
|
|
285
|
+
}
|
|
286
|
+
const parsed = JSON.parse(curl(`${apiBase}/users/${owner}/gists?per_page=100&page=${page}`));
|
|
287
|
+
if (parsed?.message) throw new Error(`unauthenticated gists list failed: ${parsed.message}`);
|
|
288
|
+
if (!Array.isArray(parsed)) throw new Error('unauthenticated gists list returned an unexpected shape');
|
|
289
|
+
pages.push(parsed); // the empty terminal page is kept: it is the receipt that the end was reached
|
|
290
|
+
if (parsed.length === 0) break;
|
|
291
|
+
}
|
|
292
|
+
return pages;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function observeGists(owner, { gh = runGh, curl = runCurl } = {}) {
|
|
296
|
+
let pages;
|
|
297
|
+
try {
|
|
298
|
+
const raw = gh(['api', `users/${owner}/gists?per_page=100`, '--paginate', '--slurp']);
|
|
299
|
+
const parsed = JSON.parse(raw);
|
|
300
|
+
pages = Array.isArray(parsed[0]) ? parsed : [parsed];
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (!/resource not accessible by integration|HTTP 403/i.test(String(error.message))) throw error;
|
|
303
|
+
pages = listGistsUnauthenticated(owner, curl);
|
|
304
|
+
}
|
|
195
305
|
const rows = pages.flat().filter((gist) => gist?.id);
|
|
306
|
+
// Identity, not completeness (A1 forbids a second completeness invariant): a gist listed twice is
|
|
307
|
+
// collection-time source movement shifting pages under the cursor, and the same id could then mask
|
|
308
|
+
// a missed one inside an equal count. Mirrors the duplicate-repository check in observeSourceUniverse.
|
|
309
|
+
const ids = rows.map((gist) => String(gist.id));
|
|
310
|
+
if (new Set(ids).size !== ids.length) throw new Error('gist enumeration returned duplicate gist identities (collection-time source movement)');
|
|
196
311
|
const expected = Number(JSON.parse(gh(['api', `users/${owner}`])).public_gists);
|
|
197
312
|
if (rows.length !== expected) throw new Error(`gist enumeration incomplete: ${rows.length}/${expected}`);
|
|
198
313
|
return {
|
|
@@ -205,17 +320,50 @@ export function observeGists(owner, { gh = runGh } = {}) {
|
|
|
205
320
|
|
|
206
321
|
function storeName(name) { return String(name).toLowerCase(); }
|
|
207
322
|
|
|
323
|
+
// A no-corpus exclusion is admissible only with INDEPENDENT source-file evidence bound to the exact
|
|
324
|
+
// revision it excludes: the file inventory actually read from the repository tree at the observed
|
|
325
|
+
// head. "0 chunks produced" is an extraction outcome, not proof of an empty source (Dual, Step 12).
|
|
326
|
+
// A record that matches pushedAt but carries no such evidence is a malformed policy entry, and it
|
|
327
|
+
// throws — it must never quietly remove a source from the collected set.
|
|
328
|
+
function assertExclusionEvidence(repo, exclusion, upstreamSha) {
|
|
329
|
+
const evidence = exclusion?.evidence;
|
|
330
|
+
const sha = String(evidence?.headSha || '');
|
|
331
|
+
if (!evidence || typeof evidence !== 'object' || !Array.isArray(evidence.files) || typeof evidence.inspectedAt !== 'string'
|
|
332
|
+
|| typeof evidence.method !== 'string') {
|
|
333
|
+
throw new Error(`no-corpus exclusion for ${repo.name} is active but carries no source-file evidence ` +
|
|
334
|
+
'(zero extracted chunks is not proof of an empty source)');
|
|
335
|
+
}
|
|
336
|
+
if (!/^[0-9a-f]{40}$/i.test(sha) || sha.toLowerCase() !== String(upstreamSha || '').toLowerCase()) {
|
|
337
|
+
throw new Error(`no-corpus exclusion for ${repo.name}: evidence headSha ${sha || '(none)'} is not the observed head ${upstreamSha}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
208
341
|
export function classifyRepository(repo, evidence, exclusion = null) {
|
|
209
|
-
const activeExclusion = exclusion && String(exclusion.pushedAt || '') !== ''
|
|
210
|
-
&& String(exclusion.pushedAt) === String(repo.pushedAt || '');
|
|
211
|
-
const disposition = activeExclusion ? 'excluded-no-corpus' : repo.isFork ? 'fork' : repo.isArchived ? 'archived' :
|
|
212
|
-
repo.isDisabled ? 'disabled' : !repo.defaultBranchRef ? 'empty' : 'eligible';
|
|
213
342
|
const upstreamSha = repo.defaultBranchRef?.target?.oid || null;
|
|
343
|
+
const activeExclusion = Boolean(exclusion && String(exclusion.pushedAt || '') !== ''
|
|
344
|
+
&& String(exclusion.pushedAt) === String(repo.pushedAt || ''));
|
|
345
|
+
if (activeExclusion) assertExclusionEvidence(repo, exclusion, upstreamSha);
|
|
346
|
+
if (repo.isFork && repo.defaultBranchRef && !repo.forkDelta) {
|
|
347
|
+
throw new Error(`fork ${repo.name} was observed without a fork delta — the observation predates policy ${SOURCE_POLICY_VERSION} or skipped the compare`);
|
|
348
|
+
}
|
|
349
|
+
// Policy 2: archives are eligible unconditionally (no `archived` branch); forks are dispositioned
|
|
350
|
+
// by their delta. Order: an explicit exclusion wins, then GitHub-side unavailability, then emptiness
|
|
351
|
+
// (a fork with no head has nothing to compare), then the fork delta, then eligible.
|
|
352
|
+
const disposition = activeExclusion ? 'excluded-no-corpus'
|
|
353
|
+
: repo.isDisabled ? 'disabled'
|
|
354
|
+
: !repo.defaultBranchRef ? 'empty'
|
|
355
|
+
: repo.isFork ? (repo.forkDelta.aheadBy > 0 ? 'fork:original-content' : 'fork:no-original-content')
|
|
356
|
+
: 'eligible';
|
|
214
357
|
const reasons = [];
|
|
215
358
|
let status = 'CURRENT';
|
|
216
|
-
if (disposition
|
|
359
|
+
if (!isIngestibleDisposition(disposition)) {
|
|
217
360
|
status = 'INELIGIBLE';
|
|
218
|
-
if (
|
|
361
|
+
if (disposition === 'excluded-no-corpus' && exclusion?.reason) reasons.push(exclusion.reason);
|
|
362
|
+
else if (disposition === 'disabled') reasons.push('repository is disabled by GitHub');
|
|
363
|
+
else if (disposition === 'empty') reasons.push('repository has no default branch — no commits to collect');
|
|
364
|
+
else if (disposition === 'fork:no-original-content') {
|
|
365
|
+
reasons.push(`fork of ${repo.forkDelta.upstream}: 0 commits ahead of upstream ${repo.forkDelta.upstreamHeadSha.slice(0, 12)} — contains no original content`);
|
|
366
|
+
}
|
|
219
367
|
}
|
|
220
368
|
else if (!evidence.rvfPresent) { status = 'MISSING'; reasons.push('canonical RVF is absent'); }
|
|
221
369
|
else if (!evidence.receipt?.sourceCommit) { status = 'UNVERIFIED'; reasons.push('RVF receipt has no sourceCommit'); }
|
|
@@ -230,6 +378,8 @@ export function classifyRepository(repo, evidence, exclusion = null) {
|
|
|
230
378
|
routing: { description: repo.description || null, homepageUrl: repo.homepageUrl || null,
|
|
231
379
|
capabilityCardPresent: evidence.cardPresent === true },
|
|
232
380
|
disposition,
|
|
381
|
+
archived: repo.isArchived === true,
|
|
382
|
+
...(repo.forkDelta ? { forkDelta: { ...repo.forkDelta } } : {}),
|
|
233
383
|
upstream: { sha: upstreamSha, committedAt: repo.defaultBranchRef?.target?.committedDate || null,
|
|
234
384
|
pushedAt: repo.pushedAt, updatedAt: repo.updatedAt },
|
|
235
385
|
artifact: { store: storeName(repo.storeName || repo.name), sourceCommit: evidence.receipt?.sourceCommit || null,
|
|
@@ -243,12 +393,19 @@ export function classifyRepository(repo, evidence, exclusion = null) {
|
|
|
243
393
|
|
|
244
394
|
export function classifyGist(gist, evidence) {
|
|
245
395
|
const source = evidence.sources?.gists?.[gist.id] || null;
|
|
246
|
-
const ingestedAt = source?.ingestedAt ||
|
|
396
|
+
const ingestedAt = source?.ingestedAt || null;
|
|
247
397
|
// The list API does not expose gist history.version. A just-fetched individual source receipt is
|
|
248
398
|
// still the authoritative version when the live list's updated_at is unchanged; otherwise the
|
|
249
399
|
// list proves drift and the row is stale until the individual gist is refreshed.
|
|
400
|
+
//
|
|
401
|
+
// 2026-09-13 (Step 4, rule 2): a flat timestamp cache (`.ruv-gists.cache.json` -- "we last saw
|
|
402
|
+
// this updated_at at some point") is NEVER consulted here anymore. It used to OR into
|
|
403
|
+
// `currentByDate`, so a gist could be classified CURRENT purely because a bare cache file claimed
|
|
404
|
+
// a date matched -- with no binding whatsoever to the gist's actual captured/rendered content.
|
|
405
|
+
// Currency is now provable ONLY from the real, already-validated per-gist source receipt (Step 2's
|
|
406
|
+
// schema-3 `ruv-gists.sources.json`): its own `updatedAt` field must equal the live list's.
|
|
250
407
|
const version = source?.updatedAt === gist.updated_at ? source.versionSha : gistVersion(gist);
|
|
251
|
-
const currentByDate = source?.updatedAt === gist.updated_at
|
|
408
|
+
const currentByDate = source?.updatedAt === gist.updated_at;
|
|
252
409
|
let status = 'CURRENT';
|
|
253
410
|
const reasons = [];
|
|
254
411
|
if (!evidence.rvfPresent) { status = 'MISSING'; reasons.push('ruv-gists RVF is absent'); }
|
|
@@ -303,6 +460,7 @@ export function renderMarkdown(coverage) {
|
|
|
303
460
|
'# RuvNet Brain source coverage', '',
|
|
304
461
|
`Generated: ${coverage.observedAt} `,
|
|
305
462
|
`Coverage generation: \`${coverage.coverageGeneration}\` `,
|
|
463
|
+
`Source policy version: ${coverage.policy?.policyVersion ?? 1} `,
|
|
306
464
|
`Repositories: ${coverage.totals.repositories} · Gists: ${coverage.totals.gists} · ` +
|
|
307
465
|
Object.entries(counts).sort().map(([state, count]) => `${state} ${count}`).join(' · '), '',
|
|
308
466
|
'> `CURRENT` is artifact-bound. Clone state and timestamps alone never establish freshness.', '',
|
|
@@ -311,7 +469,8 @@ export function renderMarkdown(coverage) {
|
|
|
311
469
|
'|---|---:|---|---:|---|---|---|',
|
|
312
470
|
];
|
|
313
471
|
for (const row of coverage.rows.filter((entry) => entry.kind === 'repository')) {
|
|
314
|
-
|
|
472
|
+
const marker = row.archived ? ' _(archived)_' : '';
|
|
473
|
+
lines.push(`| [${row.name}](${row.url})${marker} | ${row.upstream.committedAt || row.upstream.updatedAt || '—'} | ${row.upstream.sha || '—'} | ${row.artifact.ingestedAt || '—'} | ${row.artifact.sourceCommit || '—'} | ${row.status} | ${row.reasons.join('; ') || row.disposition} |`);
|
|
315
474
|
}
|
|
316
475
|
lines.push('', '## Public gists', '',
|
|
317
476
|
'| Gist | Upstream updated | Version SHA | Ingested update | State | Reason |',
|
|
@@ -337,14 +496,79 @@ export function sealCoverage({ owner, repositories, gists, rows, generatorSource
|
|
|
337
496
|
.map((status) => [status, orderedRows.filter((row) => row.status === status).length]));
|
|
338
497
|
return { schemaVersion: 1, kind: 'ruvnet-brain-corpus-coverage', owner, observedAt, generatorSourceSha,
|
|
339
498
|
sourceObservationSha256, snapshotRoot,
|
|
340
|
-
policy: { policyDispositionDigests, exemptionDigests },
|
|
499
|
+
policy: { policyVersion: SOURCE_POLICY_VERSION, policyDispositionDigests, exemptionDigests },
|
|
341
500
|
coverageGeneration: coverageGenerationFor({ generatorSourceSha, snapshotRoot, sourceObservationSha256, rows: orderedRows,
|
|
342
501
|
enumerationReceipt, policyDispositionDigests, exemptionDigests }), enumerationReceipt, rows: orderedRows,
|
|
343
502
|
totals: { repositories: repositories.expected, gists: gists.expected, rows: orderedRows.length, byStatus } };
|
|
344
503
|
}
|
|
345
504
|
|
|
346
|
-
|
|
505
|
+
// IDENTITY ACCOUNTING between a recorded coverage and a fresh one. Row keys are the stable identity
|
|
506
|
+
// (`repo:<databaseId>` for the owner's repositories — a rename keeps the key and changes the name;
|
|
507
|
+
// `repo:<owner/name>` for configured external sources, where a rename reads as remove + add; `gist:<id>`).
|
|
508
|
+
// This is how "zero unexplained omissions" is made checkable: every repository or gist that left the
|
|
509
|
+
// set since the recorded measurement is NAMED, never silently absent from a diff.
|
|
510
|
+
export function diffCoverageIdentities(recorded, current) {
|
|
511
|
+
const index = (coverage, kind) => new Map((coverage?.rows || []).filter((row) => row.kind === kind)
|
|
512
|
+
.map((row) => [row.key, row.name]));
|
|
513
|
+
const identity = ([key, name]) => ({ key, name });
|
|
514
|
+
const account = (kind, withRenames) => {
|
|
515
|
+
const before = index(recorded, kind);
|
|
516
|
+
const after = index(current, kind);
|
|
517
|
+
const result = {
|
|
518
|
+
added: [...after].filter(([key]) => !before.has(key)).map(identity),
|
|
519
|
+
removed: [...before].filter(([key]) => !after.has(key)).map(identity),
|
|
520
|
+
};
|
|
521
|
+
if (withRenames) {
|
|
522
|
+
result.renamed = [...after].filter(([key, name]) => before.has(key) && before.get(key) !== name)
|
|
523
|
+
.map(([key, to]) => ({ key, from: before.get(key), to }));
|
|
524
|
+
}
|
|
525
|
+
return result;
|
|
526
|
+
};
|
|
527
|
+
return { repositories: account('repository', true), gists: account('gist', false) };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// What `--check` prints when the recorded projection differs from the live observation: the named
|
|
531
|
+
// identity differences and the policy-version gap. A recorded coverage produced under an older
|
|
532
|
+
// SOURCE_POLICY_VERSION is stale BY CONSTRUCTION — its rows were classified by different rules — and
|
|
533
|
+
// that must be said out loud, not left as an unexplained byte mismatch.
|
|
534
|
+
export function explainCoverageDrift(recorded, current) {
|
|
535
|
+
const diff = diffCoverageIdentities(recorded, current);
|
|
536
|
+
const list = (rows) => (rows.length ? ` (${rows.map((row) => `${row.key} ${row.name}`).join(', ')})` : '');
|
|
537
|
+
const recordedPolicy = recorded?.policy?.policyVersion ?? 1;
|
|
538
|
+
const currentPolicy = current?.policy?.policyVersion ?? SOURCE_POLICY_VERSION;
|
|
539
|
+
const recordedRows = new Map((recorded?.rows || []).map((row) => [row.key, canonicalJson(row)]));
|
|
540
|
+
const changed = (current?.rows || []).filter((row) => recordedRows.has(row.key) && recordedRows.get(row.key) !== canonicalJson(row));
|
|
541
|
+
const lines = [
|
|
542
|
+
`recorded ${recorded?.observedAt || '(unknown)'} vs live ${current?.observedAt || '(unknown)'}`,
|
|
543
|
+
`rows changed in content: ${changed.length}${changed.length ? ` (first: ${changed.slice(0, 5).map((row) => row.key).join(', ')})` : ''}`,
|
|
544
|
+
`repositories: +${diff.repositories.added.length} added${list(diff.repositories.added)}, ` +
|
|
545
|
+
`-${diff.repositories.removed.length} removed${list(diff.repositories.removed)}, ` +
|
|
546
|
+
`${diff.repositories.renamed.length} renamed${diff.repositories.renamed.length
|
|
547
|
+
? ` (${diff.repositories.renamed.map((row) => `${row.key} ${row.from} -> ${row.to}`).join(', ')})` : ''}`,
|
|
548
|
+
`gists: +${diff.gists.added.length} added, -${diff.gists.removed.length} removed${list(diff.gists.removed)}`,
|
|
549
|
+
];
|
|
550
|
+
if (recordedPolicy !== currentPolicy) {
|
|
551
|
+
lines.push(`recorded policyVersion ${recordedPolicy} != current policyVersion ${currentPolicy}: the recorded ` +
|
|
552
|
+
'measurement was classified under an older eligibility policy and is stale by construction; the next pipeline ' +
|
|
553
|
+
'run will produce the new shape (archives eligible, forks dispositioned by compare, exclusions evidence-bound)');
|
|
554
|
+
}
|
|
555
|
+
return lines;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// The measured directory defaults to THE store root (kb/store-root.mjs) — the one every reader,
|
|
559
|
+
// writer and installer resolves — never `<repo>/kb`, which store-root.mjs declares a build workspace
|
|
560
|
+
// and "never a second brain". Measured 2026-09-11: three commits projected a dirty workspace as the
|
|
561
|
+
// brain (476 of 719 rows FAILED) while the canonical root matched every receipt. Policy files stay
|
|
562
|
+
// where they are source-controlled (`<repo>/kb`) unless the caller names both directories, as the
|
|
563
|
+
// release path does with `--assets`.
|
|
564
|
+
export function buildCoverage({ owner = 'ruvnet', env = process.env, home = os.homedir(), kbDir = null, policyDir = null,
|
|
347
565
|
observation = null, gh = runGh, now = () => new Date().toISOString() } = {}) {
|
|
566
|
+
policyDir ??= kbDir ?? path.join(ROOT, 'kb');
|
|
567
|
+
kbDir ??= storeRoot(env, home);
|
|
568
|
+
if (rootNeverMaterialized(kbDir)) {
|
|
569
|
+
throw new Error(`store root ${kbDir} does not exist — set RUVNET_BRAIN_KB or pass --assets <dir>; ` +
|
|
570
|
+
'the build workspace <repo>/kb is never measured by default');
|
|
571
|
+
}
|
|
348
572
|
const externalPath = path.join(policyDir, 'external-sources.json');
|
|
349
573
|
const externalPolicy = fs.existsSync(externalPath) ? JSON.parse(fs.readFileSync(externalPath, 'utf8')) : { sources: [] };
|
|
350
574
|
if (!Array.isArray(externalPolicy.sources)) throw new Error('external-sources.json has no sources array');
|
|
@@ -362,8 +586,6 @@ export function buildCoverage({ owner = 'ruvnet', kbDir = path.join(ROOT, 'kb'),
|
|
|
362
586
|
const ledger = readRvfGenerations(kbDir);
|
|
363
587
|
const cards = fs.readFileSync(path.join(kbDir, 'capability-cards.md'), 'utf8');
|
|
364
588
|
const cardStores = new Set([...cards.matchAll(/^##\s+(.+?)\s*$/gm)].map((match) => storeName(match[1])));
|
|
365
|
-
const gistCachePath = path.join(kbDir, '.ruv-gists.cache.json');
|
|
366
|
-
const gistCache = fs.existsSync(gistCachePath) ? JSON.parse(fs.readFileSync(gistCachePath, 'utf8')) : {};
|
|
367
589
|
const gistSourcesPath = path.join(kbDir, 'ruv-gists.sources.json');
|
|
368
590
|
const gistSources = fs.existsSync(gistSourcesPath) ? JSON.parse(fs.readFileSync(gistSourcesPath, 'utf8')) : null;
|
|
369
591
|
const exclusionsPath = path.join(policyDir, 'no-corpus-repos.json');
|
|
@@ -372,7 +594,7 @@ export function buildCoverage({ owner = 'ruvnet', kbDir = path.join(ROOT, 'kb'),
|
|
|
372
594
|
const store = storeName(repo.storeName || repo.name);
|
|
373
595
|
return classifyRepository(repo, artifactEvidence(kbDir, ledger, cardStores, store), exclusions[store] || null);
|
|
374
596
|
});
|
|
375
|
-
const gistEvidence = { ...artifactEvidence(kbDir, ledger, cardStores, 'ruv-gists'),
|
|
597
|
+
const gistEvidence = { ...artifactEvidence(kbDir, ledger, cardStores, 'ruv-gists'), sources: gistSources };
|
|
376
598
|
try {
|
|
377
599
|
validateGistAggregateReceipt({ receipt: gistSources,
|
|
378
600
|
passagesFile: path.join(kbDir, 'ruv-gists.passages.jsonl'),
|
|
@@ -396,20 +618,27 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
396
618
|
const ownerIndex = argv.indexOf('--owner');
|
|
397
619
|
const owner = ownerIndex >= 0 ? argv[ownerIndex + 1] : 'ruvnet';
|
|
398
620
|
const assetsIndex = argv.indexOf('--assets');
|
|
399
|
-
|
|
621
|
+
// `--assets <dir>` names a release candidate: measure it and take policy from it, exactly as before.
|
|
622
|
+
// Otherwise measure the installed brain and take policy from the repository.
|
|
623
|
+
const kbDir = assetsIndex >= 0 ? path.resolve(argv[assetsIndex + 1]) : storeRoot();
|
|
624
|
+
const policyDir = assetsIndex >= 0 ? kbDir : path.join(ROOT, 'kb');
|
|
400
625
|
const jsonPath = path.join(ROOT, 'data', 'source-coverage.json');
|
|
401
626
|
const markdownPath = path.join(ROOT, 'docs', 'RUVNET-COVERAGE.md');
|
|
402
627
|
const recorded = argv.includes('--check') && fs.existsSync(jsonPath)
|
|
403
628
|
? JSON.parse(fs.readFileSync(jsonPath, 'utf8')) : null;
|
|
404
|
-
|
|
629
|
+
console.error(`measuring ${kbDir} (policy from ${policyDir})`);
|
|
630
|
+
const coverage = buildCoverage({ owner, kbDir, policyDir,
|
|
405
631
|
now: recorded?.observedAt ? () => recorded.observedAt : () => new Date().toISOString() });
|
|
406
632
|
const json = `${JSON.stringify(coverage, null, 2)}\n`;
|
|
407
633
|
const markdown = renderMarkdown(coverage);
|
|
408
634
|
if (argv.includes('--check')) {
|
|
409
635
|
const matches = fs.existsSync(jsonPath) && fs.readFileSync(jsonPath, 'utf8') === json &&
|
|
410
636
|
fs.existsSync(markdownPath) && fs.readFileSync(markdownPath, 'utf8') === markdown;
|
|
411
|
-
const blockers = coverage.rows.filter((row) => row.disposition
|
|
412
|
-
if (!matches)
|
|
637
|
+
const blockers = coverage.rows.filter((row) => isIngestibleDisposition(row.disposition) && row.status !== 'CURRENT');
|
|
638
|
+
if (!matches) {
|
|
639
|
+
console.error('source coverage projections differ from live observation');
|
|
640
|
+
for (const line of explainCoverageDrift(recorded, coverage)) console.error(` ${line}`);
|
|
641
|
+
}
|
|
413
642
|
if (argv.includes('--strict') && blockers.length) console.error(`strict coverage: ${blockers.length} eligible row(s) are not CURRENT`);
|
|
414
643
|
return matches && (!argv.includes('--strict') || blockers.length === 0) ? 0 : 1;
|
|
415
644
|
}
|
|
@@ -49,6 +49,30 @@ const DERIVATION_MARKERS = [
|
|
|
49
49
|
'remoteBranchExists', 'installedVersion', 'verifyOutcome', 'delivered', 'succeeded ?', 'sent ===',
|
|
50
50
|
];
|
|
51
51
|
|
|
52
|
+
// Guard-clause derivation: `if (!x.ok) return {...failed...}; ... return { state: 'ok', ... };` gates
|
|
53
|
+
// the terminal literal on an earlier validator call's `.ok` result — a real derivation — but the guard
|
|
54
|
+
// can sit many lines above the literal (one guard per precondition, e.g. refreshRunHealth() checking
|
|
55
|
+
// registration, identity, and envelope validity in turn before its one unconditional success return).
|
|
56
|
+
// That is outside the local ±2-line window designed for inline ternaries, so it false-positived on
|
|
57
|
+
// plugin/scripts/nightly-scheduler.mjs:271 (caught by the 2026-09-12 consistency audit; state:'ok' IS
|
|
58
|
+
// gated, just structurally distant). Recognize the guard-clause idiom explicitly instead of widening
|
|
59
|
+
// the generic window — widening would also swallow unrelated markers anywhere in the function and
|
|
60
|
+
// could hide a genuinely naked literal sitting next to unrelated derivation-looking text.
|
|
61
|
+
const OK_GUARD_RE = /\bif\s*\(\s*!\s*[\w$]+(?:\.[\w$]+)*\.ok\b\s*\)\s*return\b/;
|
|
62
|
+
const FUNCTION_START_RE = /^\s*(export\s+)?(default\s+)?(async\s+)?function\b/;
|
|
63
|
+
|
|
64
|
+
/** True if an earlier `if (!validator(...).ok) return ...;` guard, within the innermost enclosing
|
|
65
|
+
* top-level function, precedes line `matchIndex` — i.e. the function cannot reach `matchIndex`
|
|
66
|
+
* without that validator call having already passed. */
|
|
67
|
+
function hasEnclosingOkGuard(lines, matchIndex) {
|
|
68
|
+
let start = 0;
|
|
69
|
+
for (let j = matchIndex - 1; j >= 0; j--) {
|
|
70
|
+
if (FUNCTION_START_RE.test(lines[j])) { start = j; break; }
|
|
71
|
+
}
|
|
72
|
+
for (let j = start; j < matchIndex; j++) if (OK_GUARD_RE.test(lines[j])) return true;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
52
76
|
/** Scan one source text. Returns violations: [{line, text}] — success literal with no derivation. */
|
|
53
77
|
export function scanSource(src, _name = '(inline)') {
|
|
54
78
|
const violations = [];
|
|
@@ -61,6 +85,7 @@ export function scanSource(src, _name = '(inline)') {
|
|
|
61
85
|
// The statement window: this line ± 2 (multi-line object literals / sh case arms).
|
|
62
86
|
const windowText = lines.slice(Math.max(0, i - 2), i + 3).join('\n');
|
|
63
87
|
if (DERIVATION_MARKERS.some((m) => windowText.includes(m))) continue;
|
|
88
|
+
if (hasEnclosingOkGuard(lines, i)) continue;
|
|
64
89
|
violations.push({ line: i + 1, text: t.slice(0, 160) });
|
|
65
90
|
}
|
|
66
91
|
return violations;
|
package/scripts/sync-census.mjs
CHANGED
|
Binary file
|
package/scripts/sync-version.mjs
CHANGED
|
@@ -175,6 +175,8 @@ if (CHECK) {
|
|
|
175
175
|
'tests/unit/version.test.mjs', 'tests/unit/sync-version-drift.test.mjs', 'tests/unit/self-update-plan.test.mjs',
|
|
176
176
|
// Asserts stripTag('v1.14.1-dev') === '1.14.1-dev' — the literals ARE the fixture.
|
|
177
177
|
'tests/unit/version-tag-vs-field.test.mjs',
|
|
178
|
+
// MCP reload tests carry version literals in fixture manifests — these are test payloads, not production code.
|
|
179
|
+
'tests/unit/mcp-graceful-reload.test.mjs', 'tests/integration/mcp-reload-no-restart-required.test.mjs',
|
|
178
180
|
]);
|
|
179
181
|
const litRe = /['"`](v?\d+\.\d+\.\d+(?:-dev)?)['"`]/g;
|
|
180
182
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** First-class RuvNet Brain entry point for Dual/TriSmart reviews. */
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const DUAL = path.join(ROOT, 'dual-host-deliberation.mjs');
|
|
9
|
+
const TRISMART = path.resolve(ROOT, '..', 'tri-smart-skill', 'tri-smart', 'scripts', 'review.mjs');
|
|
10
|
+
|
|
11
|
+
function usage() {
|
|
12
|
+
console.log('Usage: node scripts/trismart.mjs [--mode=auto|dual|tri] [--dry-run] <architecture task>');
|
|
13
|
+
console.log('Dual uses Claude Code + Codex; TriSmart adds Grok when its native CLI subscription is verified.');
|
|
14
|
+
console.log('All provider API-key variables are removed before a provider process starts.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
if (args.includes('--help') || args.includes('-h')) { usage(); process.exit(0); }
|
|
19
|
+
const mode = args.find((arg) => arg.startsWith('--mode='))?.slice(7) ?? 'auto';
|
|
20
|
+
if (!['auto', 'dual', 'tri'].includes(mode)) { console.error('mode must be auto, dual, or tri'); process.exit(2); }
|
|
21
|
+
const dryRun = args.includes('--dry-run');
|
|
22
|
+
const task = args.filter((arg) => !arg.startsWith('--')).join(' ').trim();
|
|
23
|
+
if (!task) { usage(); process.exit(2); }
|
|
24
|
+
|
|
25
|
+
if (dryRun) {
|
|
26
|
+
const providers = mode === 'dual' ? ['claude', 'codex'] : ['claude', 'codex', 'grok'];
|
|
27
|
+
console.log(JSON.stringify({ status: 'ready', mode, providers, models: {
|
|
28
|
+
claude: 'claude-fable-5-1', codex: 'gpt-6-astra', grok: 'grok-4.6',
|
|
29
|
+
}, billingPath: 'native subscription/OAuth CLI; API-key variables unset', taskHash: task.length }, null, 2));
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The portable TriSmart runner owns three-provider probing and truthful degraded output.
|
|
34
|
+
// The brain's Dual path remains the native two-host implementation and is kept separate so
|
|
35
|
+
// a missing Grok CLI can never silently downgrade a requested TriSmart review.
|
|
36
|
+
if (mode === 'tri' || mode === 'auto') {
|
|
37
|
+
const result = spawnSync(process.execPath, [TRISMART, `--mode=${mode}`, task], { stdio: 'inherit' });
|
|
38
|
+
process.exitCode = result.status ?? 1;
|
|
39
|
+
} else {
|
|
40
|
+
const result = spawnSync(process.execPath, [DUAL, task], { stdio: 'inherit' });
|
|
41
|
+
process.exitCode = result.status ?? 1;
|
|
42
|
+
}
|