ruvnet-brain 4.0.90-dev → 4.3.1-dev
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 +31 -7
- package/bin/install.mjs +153 -22
- package/console/architecture.html +12 -14
- package/console/install-mockup.html +3 -3
- package/kb/zip-extract.mjs +8 -2
- package/package.json +7 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +1 -1
- package/plugin/hooks/hooks.json +10 -0
- package/plugin/host-adapters/claude.json +11 -0
- package/plugin/host-adapters/codex.json +11 -0
- package/plugin/mcp/managed-cli-interface.mjs +77 -10
- package/plugin/mcp/server.mjs +2 -1
- package/plugin/scripts/anticipate.sh +30 -1
- package/plugin/scripts/capability-claim-evidence.mjs +431 -0
- package/plugin/scripts/capability-inventory-receipt.mjs +225 -0
- package/plugin/scripts/capability-registry.mjs +6 -0
- package/plugin/scripts/capability-routing.mjs +70 -0
- package/plugin/scripts/continuation-gate.mjs +68 -2
- package/plugin/scripts/coverage-integrity.mjs +575 -0
- package/plugin/scripts/decision-outcomes.mjs +0 -0
- package/plugin/scripts/ground-ruvnet.sh +2 -2
- package/plugin/scripts/nightly-controller.mjs +19 -2
- package/plugin/scripts/project-progression-contract.mjs +409 -0
- package/plugin/scripts/project-progression-hook.mjs +196 -0
- package/plugin/scripts/project-progression-outbox.mjs +95 -0
- package/plugin/scripts/project-progression-session-start.mjs +183 -0
- package/plugin/scripts/project-progression-store.mjs +248 -0
- package/plugin/scripts/project-store-resolver.mjs +106 -0
- package/plugin/scripts/session-snapshot-hook.mjs +29 -1
- package/plugin/scripts/session-start-core.mjs +22 -18
- package/plugin/scripts/spend-guard.mjs +6 -1
- package/scripts/adr-072-completion.mjs +98 -0
- package/scripts/behavioral-l1-l4.mjs +11 -8
- package/scripts/brain-score.mjs +23 -9
- package/scripts/build-bundle.mjs +94 -8
- package/scripts/card-from-source.mjs +140 -0
- package/scripts/corpus-aggregates.mjs +194 -0
- package/scripts/corpus-candidate.mjs +314 -0
- package/scripts/corpus-reconcile.mjs +540 -0
- package/scripts/corpus-seed-publish.mjs +110 -0
- package/scripts/coverage-integrity.mjs +3 -0
- package/scripts/gist-receipts.mjs +183 -0
- package/scripts/git-hooks/pre-push +21 -0
- package/scripts/host-registry.mjs +118 -0
- package/scripts/independent-review-receipt.mjs +499 -0
- package/scripts/ingest-gists.mjs +34 -5
- package/scripts/ingest-new-repos.mjs +127 -0
- package/scripts/metaharness-gate.mjs +58 -0
- package/scripts/nightly-gists.sh +3 -3
- package/scripts/nightly-wrapper.sh +105 -64
- package/scripts/product-integrity-contract.mjs +156 -0
- package/scripts/public-inventory.mjs +4 -0
- package/scripts/public-verification-aggregate.mjs +342 -0
- package/scripts/public-verification-finalizer.mjs +59 -0
- package/scripts/public-verification-inputs.mjs +473 -0
- package/scripts/public-verification-lane.mjs +244 -0
- package/scripts/publication-receipt.mjs +50 -6
- package/scripts/rebuild-gists-from-receipts.mjs +291 -0
- package/scripts/release-projection.mjs +86 -0
- package/scripts/release-transaction-provider.mjs +43 -102
- package/scripts/release-transaction.mjs +165 -49
- package/scripts/release.mjs +158 -3
- package/scripts/restore-local-ingests.mjs +2 -2
- package/scripts/retrieval-canary.mjs +533 -0
- package/scripts/routing-flywheel.mjs +12 -1
- package/scripts/rvf-generation.mjs +50 -0
- package/scripts/self-update.mjs +19 -44
- package/scripts/source-coverage.mjs +415 -0
- package/scripts/source-scope-receipt.mjs +84 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/wired-check.mjs +114 -24
- package/scripts/worktree-integrity.mjs +123 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ingest-new-repos.mjs — load what rUv shipped, without being asked.
|
|
4
|
+
*
|
|
5
|
+
* THE GAP THIS CLOSES, and it is embarrassing rather than subtle. The owner asked on 2026-08-20
|
|
6
|
+
* why `ruvnet/ultrasonic` was not in the brain: *"aren't you loading everything Ruv creates every
|
|
7
|
+
* day?????"* The honest answer was no, and nothing ever had. `nightly-wrapper.sh` refreshed
|
|
8
|
+
* lessons, health, the release watchdog and the replay proof — and contained ZERO references to
|
|
9
|
+
* ingestion. New rUv repos only ever entered the brain when a human typed `ingest-repo.mjs` by
|
|
10
|
+
* hand, so the corpus drifted behind the org silently and the only symptom was a question the
|
|
11
|
+
* brain could not answer.
|
|
12
|
+
*
|
|
13
|
+
* `brain-stamp.mjs` already COMPUTES built-vs-pending every night. Nothing acted on it. Measuring
|
|
14
|
+
* a gap and never closing it is how 137 repos stayed uningested while the scorecard reported the
|
|
15
|
+
* number cheerfully.
|
|
16
|
+
*
|
|
17
|
+
* WHY IT IS BOUNDED. Ingestion embeds a whole repository; doing 137 in one night would run for
|
|
18
|
+
* hours and starve everything after it in the nightly. `--max` (default 3) keeps the nightly
|
|
19
|
+
* bounded and lets the corpus converge over days instead of blocking on one enormous run. That is
|
|
20
|
+
* a deliberate trade, and the run SAYS how many remain rather than implying it finished.
|
|
21
|
+
*
|
|
22
|
+
* WHAT IT WILL NOT DO. It does not write capability cards. An ingested store with no card is DARK —
|
|
23
|
+
* valid bytes that no by-description query can reach — and `ingest-repo.mjs` already says so on
|
|
24
|
+
* every run. Inventing a card from a repo name would put a confident description in the routing
|
|
25
|
+
* layer that nobody grounded in the source, which is worse than an honest gap: it would route real
|
|
26
|
+
* questions to a corpus that cannot answer them. So this reports the dark stores it created and
|
|
27
|
+
* leaves the card to someone who reads the repo.
|
|
28
|
+
*
|
|
29
|
+
* node scripts/ingest-new-repos.mjs # report only
|
|
30
|
+
* node scripts/ingest-new-repos.mjs --apply # ingest up to --max new repos
|
|
31
|
+
* node scripts/ingest-new-repos.mjs --apply --max 5
|
|
32
|
+
*/
|
|
33
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
import { fileURLToPath } from 'node:url';
|
|
36
|
+
import { storeRoot, storesAt, darkStores } from '../kb/store-root.mjs';
|
|
37
|
+
import { assertIsolatedMutationWorktree } from './worktree-integrity.mjs';
|
|
38
|
+
|
|
39
|
+
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
40
|
+
const OWNER = process.env.RUVNET_ORG_OWNER || 'ruvnet';
|
|
41
|
+
const APPLY = process.argv.includes('--apply');
|
|
42
|
+
const arg = (n, d) => {
|
|
43
|
+
const i = process.argv.indexOf(n);
|
|
44
|
+
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d;
|
|
45
|
+
};
|
|
46
|
+
const MAX = Number(arg('--max', '3'));
|
|
47
|
+
|
|
48
|
+
/** Every repo the org actually has right now. Live, never a remembered list. */
|
|
49
|
+
function liveOrgRepos() {
|
|
50
|
+
try {
|
|
51
|
+
const out = execFileSync('gh', [
|
|
52
|
+
'repo', 'list', OWNER, '--limit', '500', '--no-archived', '--json', 'name,isFork,pushedAt,diskUsage',
|
|
53
|
+
], { cwd: ROOT, encoding: 'utf8', maxBuffer: 1 << 26 });
|
|
54
|
+
return JSON.parse(out)
|
|
55
|
+
.filter((r) => !r.isFork)
|
|
56
|
+
// AN EMPTY REPO IS NOT A FAILURE TO RETRY FOREVER (measured 2026-08-20).
|
|
57
|
+
//
|
|
58
|
+
// The first bulk run reported 7 failures; six of them are size=0KB — genuinely empty
|
|
59
|
+
// repositories (socket, Auto-GPT, CodeGPT, ruvGPT2, rUvGPT, AIConverse). `git fetch` succeeds
|
|
60
|
+
// and there is simply nothing to embed, so ingestion fails correctly and then the repo stays
|
|
61
|
+
// "missing", so the NEXT night retries it, forever. A permanent nightly failure that is
|
|
62
|
+
// actually correct behaviour trains the reader to ignore the failure line — which is how a
|
|
63
|
+
// real failure would then hide inside it.
|
|
64
|
+
.filter((r) => Number(r.diskUsage ?? 1) > 0)
|
|
65
|
+
// Newest first: if the budget only allows a few tonight, spend it on what rUv shipped most
|
|
66
|
+
// recently, which is what a question is most likely to be about.
|
|
67
|
+
.sort((a, b) => String(b.pushedAt).localeCompare(String(a.pushedAt)))
|
|
68
|
+
.map((r) => r.name);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return { error: error?.message || String(error) };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function pendingRepos(orgRepos, root = storeRoot()) {
|
|
75
|
+
const have = new Set(storesAt(root).map((s) => String(s).toLowerCase()));
|
|
76
|
+
return orgRepos.filter((name) => !have.has(String(name).toLowerCase()));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const isMain = (() => {
|
|
80
|
+
try { return process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); }
|
|
81
|
+
catch { return false; }
|
|
82
|
+
})();
|
|
83
|
+
|
|
84
|
+
if (isMain) {
|
|
85
|
+
if (APPLY) {
|
|
86
|
+
try { assertIsolatedMutationWorktree(ROOT, 'ingest-new-repos --apply'); }
|
|
87
|
+
catch (error) { console.error(error?.message || String(error)); process.exit(2); }
|
|
88
|
+
}
|
|
89
|
+
const repos = liveOrgRepos();
|
|
90
|
+
if (!Array.isArray(repos)) {
|
|
91
|
+
// A LIST WE COULD NOT FETCH IS NOT AN EMPTY LIST. Reporting "0 new repos" here would read as
|
|
92
|
+
// "the brain is current" when the truth is "I could not look" — the exact conflation this
|
|
93
|
+
// repository keeps paying for.
|
|
94
|
+
console.error(`[ingest-new] could NOT list ${OWNER}'s repos: ${repos.error}`);
|
|
95
|
+
console.error('[ingest-new] this is NOT "nothing new" — it is "I could not look". Exiting 1.');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const root = storeRoot();
|
|
100
|
+
const missing = pendingRepos(repos, root);
|
|
101
|
+
console.log(`[ingest-new] ${OWNER}: ${repos.length} live repos, ${storesAt(root).length} ingested, ${missing.length} missing`);
|
|
102
|
+
if (!missing.length) { console.log('[ingest-new] corpus is level with the org.'); process.exit(0); }
|
|
103
|
+
|
|
104
|
+
console.log(`[ingest-new] next up (newest first): ${missing.slice(0, MAX).join(', ')}`);
|
|
105
|
+
if (!APPLY) {
|
|
106
|
+
console.log(`[ingest-new] report only. Re-run with --apply to ingest up to ${MAX}.`);
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let done = 0; let failed = 0;
|
|
111
|
+
for (const name of missing.slice(0, MAX)) {
|
|
112
|
+
console.log(`\n[ingest-new] ingesting ${name}`);
|
|
113
|
+
const r = spawnSync(process.execPath, [path.join(ROOT, 'scripts', 'ingest-repo.mjs'), '--name', name],
|
|
114
|
+
{ cwd: ROOT, stdio: 'inherit', env: process.env, timeout: 45 * 60_000 });
|
|
115
|
+
if (r.status === 0) done += 1;
|
|
116
|
+
else { failed += 1; console.log(`[ingest-new] ${name} FAILED — left missing so the next run retries it`); }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const stillMissing = missing.length - done;
|
|
120
|
+
const dark = darkStores(root);
|
|
121
|
+
console.log(`\n[ingest-new] ingested ${done}, failed ${failed}, ${stillMissing} still missing.`);
|
|
122
|
+
if (dark.length) {
|
|
123
|
+
console.log(`[ingest-new] ${dark.length} store(s) are DARK — valid bytes no by-description query can reach.`);
|
|
124
|
+
console.log('[ingest-new] a card is a claim about what a repo is FOR; write it from the source, not the name.');
|
|
125
|
+
}
|
|
126
|
+
process.exit(failed ? 1 : 0);
|
|
127
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Compose measured MetaHarness read surfaces into one fail-closed gate. */
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
const REQUIRED_SCORE_FIELDS = Object.freeze(['harnessFit', 'compileConfidence', 'taskCoverage', 'toolSafety', 'memoryUsefulness']);
|
|
6
|
+
function unwrap(value) { return value && value.data && typeof value.data === 'object' ? value.data : value; }
|
|
7
|
+
function finite(value) { return typeof value === 'number' && Number.isFinite(value) ? value : null; }
|
|
8
|
+
function severity(value) { const key = String(value ?? '').toLowerCase(); return ['clean', 'low', 'medium', 'high'].includes(key) ? key : null; }
|
|
9
|
+
function digest(value) { return crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex'); }
|
|
10
|
+
|
|
11
|
+
export function summarizeScore(input) {
|
|
12
|
+
const score = unwrap(input);
|
|
13
|
+
if (!score || score.degraded === true) return { status: 'unmeasured', degraded: true, reason: score?.reason || 'score unavailable' };
|
|
14
|
+
const dimensions = Object.fromEntries(REQUIRED_SCORE_FIELDS.map((key) => [key, finite(score[key])]));
|
|
15
|
+
const missing = REQUIRED_SCORE_FIELDS.filter((key) => dimensions[key] === null);
|
|
16
|
+
return { status: missing.length ? 'unmeasured' : 'measured', dimensions, missing, costPerRunUsd: finite(score.estCostPerRunUsd), recommendedMode: typeof score.recommendedMode === 'string' ? score.recommendedMode : null, scaffoldReady: typeof score.scaffoldReady === 'boolean' ? score.scaffoldReady : null };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function summarizeAudit(input) {
|
|
20
|
+
const audit = unwrap(input);
|
|
21
|
+
const worst = severity(audit?.composite?.worst ?? audit?.worst);
|
|
22
|
+
const degraded = audit?.degraded === true || Object.values(audit?.components || {}).some((v) => v?.degraded === true);
|
|
23
|
+
return { status: worst ? 'measured' : 'unmeasured', worst, degraded, fingerprint: typeof audit?.fingerprint === 'string' ? audit.fingerprint : null };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function summarizeDrift(input) {
|
|
27
|
+
const drift = unwrap(input);
|
|
28
|
+
const alert = typeof drift?.alert === 'boolean' ? drift.alert : null;
|
|
29
|
+
const verdict = typeof drift?.verdict === 'string' ? drift.verdict : null;
|
|
30
|
+
return { status: alert === null && !verdict ? 'unmeasured' : 'measured', alert, verdict, similarity: finite(drift?.similarity), reason: drift?.reason || null };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function summarizeRoutingReceipts(rows = []) {
|
|
34
|
+
if (!Array.isArray(rows)) return { status: 'unmeasured', count: 0, costUsd: null, savedUsd: null };
|
|
35
|
+
const valid = rows.filter((r) => r && typeof r === 'object' && typeof r.model === 'string');
|
|
36
|
+
const withCost = valid.filter((r) => finite(r.est_cost) !== null);
|
|
37
|
+
const withSaved = valid.filter((r) => finite(r.saved) !== null);
|
|
38
|
+
return { status: valid.length ? 'measured' : 'unmeasured', count: valid.length, costUsd: withCost.length ? withCost.reduce((sum, r) => sum + r.est_cost, 0) : null, savedUsd: withSaved.length ? withSaved.reduce((sum, r) => sum + r.saved, 0) : null, unpriced: valid.length - withCost.length };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Read-only composition. `allowMetered` defaults false: readiness never authorizes spend. */
|
|
42
|
+
export function buildGateReceipt({ score, audit, drift, routingReceipts = [], candidate = null, allowMetered = false, now = new Date().toISOString() } = {}) {
|
|
43
|
+
const measuredScore = summarizeScore(score), measuredAudit = summarizeAudit(audit), measuredDrift = summarizeDrift(drift), routing = summarizeRoutingReceipts(routingReceipts);
|
|
44
|
+
const reasons = [];
|
|
45
|
+
if (measuredScore.status !== 'measured') reasons.push('score is missing required measured dimensions');
|
|
46
|
+
if (measuredAudit.status !== 'measured') reasons.push('OIA audit has no measured composite severity');
|
|
47
|
+
if (measuredAudit.degraded) reasons.push('OIA audit is degraded');
|
|
48
|
+
if (measuredDrift.status !== 'measured') reasons.push('drift result is unmeasured');
|
|
49
|
+
if (measuredDrift.alert === true || /major|high|regress/i.test(measuredDrift.verdict || '')) reasons.push('drift is alerting');
|
|
50
|
+
if (measuredAudit.worst === 'high') reasons.push('OIA audit is high severity');
|
|
51
|
+
const ready = reasons.length === 0;
|
|
52
|
+
const candidateVerified = candidate?.bundle_verified === true && candidate?.data_source === 'SYNTHETIC';
|
|
53
|
+
const receipt = { schemaVersion: 1, kind: 'metaharness-gate', ts: now, score: measuredScore, audit: measuredAudit, drift: measuredDrift, routing, readiness: ready ? 'ready' : 'blocked', evolution: { allowed: ready && allowMetered === true, reason: ready ? (allowMetered ? 'measured preflight passed and metered spend explicitly allowed' : 'metered spend is disabled') : reasons.join('; ') }, promotion: { allowed: ready && candidateVerified, reason: !ready ? reasons.join('; ') : candidateVerified ? 'verified synthetic replay candidate' : 'candidate is absent or not a verified synthetic replay' } };
|
|
54
|
+
return { ...receipt, evidenceDigest: digest(receipt) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function assertEvolutionAllowed(receipt) { if (!receipt?.evolution?.allowed) throw new Error(`MetaHarness evolution blocked: ${receipt?.evolution?.reason || 'missing gate receipt'}`); return receipt; }
|
|
58
|
+
export function assertPromotionAllowed(receipt) { if (!receipt?.promotion?.allowed) throw new Error(`MetaHarness promotion blocked: ${receipt?.promotion?.reason || 'missing gate receipt'}`); return receipt; }
|
package/scripts/nightly-gists.sh
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
# 3. re-embed — the expensive step (~18 min, 8 shards). SKIPPED ENTIRELY when step 2
|
|
11
11
|
# reports "nothing to do". A quiet night costs seconds, not minutes.
|
|
12
12
|
#
|
|
13
|
-
# PATH is set explicitly
|
|
14
|
-
#
|
|
15
|
-
# /opt/homebrew/bin and launchd
|
|
13
|
+
# PATH is set explicitly because launchd does not inherit a login shell's environment. The retired
|
|
14
|
+
# com.ruvnet.brain-nightly source writer previously exposed this exact failure mode: gh lived in
|
|
15
|
+
# /opt/homebrew/bin and launchd could not see it.
|
|
16
16
|
|
|
17
17
|
set -eu
|
|
18
18
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# nightly-wrapper.sh —
|
|
2
|
+
# nightly-wrapper.sh — author-side maintenance, intentionally NOT a primary-checkout scheduler.
|
|
3
|
+
# The old com.ruvnet.brain-nightly LaunchAgent invoked this in the primary checkout and accumulated
|
|
4
|
+
# generated source changes beside active human work. That job is retired. The supported scheduled
|
|
5
|
+
# updater is com.ruvnet.brain-update and mutates only the installed cache. If this author harness is
|
|
6
|
+
# run manually, every source-writing command below requires a clean linked worktree.
|
|
3
7
|
# (2026-07-12): no noise for success or no-op — only ACTIVE alerts for real failure, and the system
|
|
4
8
|
# must attempt to self-heal before escalating, then make failure impossible to miss next session.
|
|
5
9
|
#
|
|
@@ -7,26 +11,69 @@
|
|
|
7
11
|
# class a blind retry can fix) -> if it fails AGAIN, loud phone alert + a durable marker file that
|
|
8
12
|
# ground-ruvnet.sh surfaces at the top of the very next session, unprompted. Success or a legitimate
|
|
9
13
|
# "nothing was due tonight" no-op: log only, phone stays silent.
|
|
14
|
+
|
|
15
|
+
# WHICH NODE RUNS THIS DECIDES WHETHER TONIGHT'S WRITES SURVIVE (measured 2026-08-20).
|
|
16
|
+
#
|
|
17
|
+
# This file hardcoded "$NODE_BIN" in eight places. That is node v22.13.1 (ABI 127), and the
|
|
18
|
+
# better-sqlite3 binding agentdb needs is built for ABI 137 — so under it agentdb SILENTLY falls
|
|
19
|
+
# back to non-persistent sql.js and every write is lost. Silently, because better-sqlite3 is an
|
|
20
|
+
# OPTIONAL dependency (ruvnet/ruflo#2219): the failure never errors and the CLI still prints a
|
|
21
|
+
# success table. `lesson-bridge.mjs --apply` and `learning-replay.mjs` both write, so the nightly
|
|
22
|
+
# was discarding its own results. Measured, same command, same DB, only the interpreter differing:
|
|
23
|
+
# "$NODE_BIN" v22.13.1 -> 2 sql.js fallback msgs, DURABLE=NO
|
|
24
|
+
# /opt/homebrew/bin/node v24.18.0 -> 0 fallback msgs, DURABLE=YES
|
|
25
|
+
# Resolve a node whose ABI matches the binding, and fail loudly rather than silently losing data.
|
|
26
|
+
NODE_BIN=""
|
|
27
|
+
for _cand in /opt/homebrew/opt/node@24/bin/node /opt/homebrew/bin/node "$(command -v node 2>/dev/null)"; do
|
|
28
|
+
[ -x "$_cand" ] || continue
|
|
29
|
+
if [ "$("$_cand" -p 'process.versions.modules' 2>/dev/null)" = "137" ]; then NODE_BIN="$_cand"; break; fi
|
|
30
|
+
done
|
|
31
|
+
if [ -z "$NODE_BIN" ]; then
|
|
32
|
+
echo "[nightly] FATAL: no node with ABI 137 found — agentdb writes would silently not persist." >&2
|
|
33
|
+
echo "[nightly] checked: /opt/homebrew/opt/node@24/bin/node /opt/homebrew/bin/node \$(command -v node)" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
10
37
|
set -u
|
|
11
38
|
# kb/models-cache (the fallback) starts cold every time -> every nightly re-downloads the ONNX
|
|
12
39
|
# embedder from HuggingFace. Point at the already-warm cache instead (verified present).
|
|
13
40
|
export KB_MODEL_CACHE="/Users/stuartkerr/Code/PowerPlatePulse/scripts/models-cache"
|
|
14
|
-
cd
|
|
41
|
+
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" 2>/dev/null && pwd)
|
|
42
|
+
WORKTREE_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." 2>/dev/null && pwd)
|
|
43
|
+
cd "$WORKTREE_ROOT" 2>/dev/null || {
|
|
15
44
|
curl -sS --max-time 10 -H "Title: 🔴 Nightly CRASHED before it could even start" \
|
|
16
45
|
-H "Priority: urgent" -H "Tags: rotating_light" \
|
|
17
46
|
-d "cd into the repo failed — filesystem or mount problem. Investigate the machine directly." \
|
|
18
47
|
"https://ntfy.sh/$(grep -m1 '^NTFY_TOPIC=' /Users/stuartkerr/Code/ruvnet-brain/.env 2>/dev/null | cut -d= -f2)" >/dev/null 2>&1
|
|
19
48
|
exit 1
|
|
20
49
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
50
|
+
GIT_COMMON_DIR=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) || exit 1
|
|
51
|
+
PRIMARY_ROOT=$(dirname "$GIT_COMMON_DIR")
|
|
52
|
+
export RUVNET_PROJECT_MEMORY_DB="$PRIMARY_ROOT/.swarm/memory.db"
|
|
53
|
+
mkdir -p "$PRIMARY_ROOT/logs" "$PRIMARY_ROOT/.ruvnet-brain"
|
|
54
|
+
LOG="$PRIMARY_ROOT/logs/nightly.log"
|
|
55
|
+
MARKER="$PRIMARY_ROOT/.ruvnet-brain/nightly-failure.json"
|
|
56
|
+
LOCK="$PRIMARY_ROOT/.ruvnet-brain/nightly.lock"
|
|
57
|
+
PRIMARY_SNAPSHOT=$(mktemp "${TMPDIR:-/tmp}/ruvnet-brain-primary-snapshot.XXXXXX") || exit 1
|
|
58
|
+
if ! "$NODE_BIN" scripts/worktree-integrity.mjs snapshot "$WORKTREE_ROOT" > "$PRIMARY_SNAPSHOT"; then
|
|
59
|
+
rm -f "$PRIMARY_SNAPSHOT"
|
|
60
|
+
exit 2
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
finish() {
|
|
64
|
+
rc=$?
|
|
65
|
+
trap - EXIT
|
|
66
|
+
rm -f "$LOCK"
|
|
67
|
+
if ! "$NODE_BIN" scripts/worktree-integrity.mjs verify "$WORKTREE_ROOT" "$PRIMARY_SNAPSHOT" >> "$LOG" 2>&1; then
|
|
68
|
+
echo "===== FATAL: primary checkout changed during isolated author maintenance =====" >> "$LOG"
|
|
69
|
+
rc=3
|
|
70
|
+
fi
|
|
71
|
+
rm -f "$PRIMARY_SNAPSHOT"
|
|
72
|
+
exit "$rc"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Single-instance guard for explicit author invocations. A stale lock from a crashed run (PID no
|
|
76
|
+
# longer alive) is treated as no lock.
|
|
30
77
|
if [ -f "$LOCK" ]; then
|
|
31
78
|
OLD_PID=$(cat "$LOCK" 2>/dev/null)
|
|
32
79
|
if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then
|
|
@@ -38,26 +85,22 @@ if [ -f "$LOCK" ]; then
|
|
|
38
85
|
echo "===== $(date -u +%FT%TZ) — stale lock from pid $OLD_PID (not running) — clearing =====" >> "$LOG"
|
|
39
86
|
fi
|
|
40
87
|
echo $$ > "$LOCK"
|
|
41
|
-
trap
|
|
42
|
-
|
|
43
|
-
# NIGHTLY_SMOKE=1
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# WHY IT EXISTS (2026-07-13): the 03:15 fire on 07-12 died before it could even write its own log
|
|
47
|
-
# ("/bin/bash: logs/nightly.log: No such file or directory" — relative path, no working directory) and
|
|
48
|
-
# NOBODY KNEW, because the only way to test this chain was to wait until 03:15 and hope. A scheduled job
|
|
49
|
-
# you can only test by waiting for it is a job you are not testing. Now: `NIGHTLY_SMOKE=1 <the exact
|
|
50
|
-
# plist command>` proves it on demand, any time, in seconds.
|
|
88
|
+
trap finish EXIT
|
|
89
|
+
|
|
90
|
+
# NIGHTLY_SMOKE=1 exercises the author harness and source planner without a multi-hour rebuild. It
|
|
91
|
+
# still requires an isolated clean linked worktree: a diagnostic escape hatch must not reopen the
|
|
92
|
+
# primary-checkout writer. Publication is never part of this job.
|
|
51
93
|
SMOKE="${NIGHTLY_SMOKE:-0}"
|
|
94
|
+
"$NODE_BIN" scripts/worktree-integrity.mjs "$WORKTREE_ROOT" "nightly author maintenance" >> "$LOG" 2>&1 || exit 2
|
|
52
95
|
|
|
53
96
|
run_once() {
|
|
54
97
|
local rc
|
|
55
98
|
{ echo "===== nightly-wrapper rebuild attempt $1 — $(date -u +%FT%TZ) ====="
|
|
56
99
|
if [ "$SMOKE" = "1" ]; then
|
|
57
100
|
echo "[SMOKE] dry-run: exercising the full chain, NOT building"
|
|
58
|
-
|
|
101
|
+
"$NODE_BIN" scripts/self-update.mjs --fresh-window 60
|
|
59
102
|
else
|
|
60
|
-
|
|
103
|
+
"$NODE_BIN" scripts/self-update.mjs --apply --fresh-window 60
|
|
61
104
|
fi
|
|
62
105
|
} >> "$LOG" 2>&1
|
|
63
106
|
rc=$?
|
|
@@ -75,13 +118,42 @@ run_once() {
|
|
|
75
118
|
return 0
|
|
76
119
|
}
|
|
77
120
|
|
|
121
|
+
# Prepare the source candidate before any optional evidence refresh that writes tracked artifacts.
|
|
122
|
+
# Otherwise learning-replay dirties the isolated lane first and self-update correctly refuses it.
|
|
123
|
+
if run_once 1; then
|
|
124
|
+
if [ "$SMOKE" = "1" ]; then exit 0; fi
|
|
125
|
+
else
|
|
126
|
+
echo "===== first attempt failed — waiting 3 min, retrying once (self-heal for transient issues) =====" >> "$LOG"
|
|
127
|
+
sleep 180
|
|
128
|
+
if run_once 2; then
|
|
129
|
+
echo "===== SELF-HEALED on retry — no escalation needed =====" >> "$LOG"
|
|
130
|
+
else
|
|
131
|
+
# Both attempts genuinely failed. Escalate loudly AND leave a marker the next session cannot miss.
|
|
132
|
+
AFTER=$(gh release view --json tagName -q .tagName 2>/dev/null || echo "unknown")
|
|
133
|
+
TAIL=$(tail -25 "$LOG" | tr '\n' ' ' | cut -c1-2000)
|
|
134
|
+
mkdir -p "$(dirname "$MARKER")"
|
|
135
|
+
python3 -c "
|
|
136
|
+
import json, datetime
|
|
137
|
+
json.dump({
|
|
138
|
+
'at': datetime.datetime.utcnow().isoformat() + 'Z',
|
|
139
|
+
'last_release_tag_observed': '$AFTER',
|
|
140
|
+
'tail': '''$TAIL''',
|
|
141
|
+
'note': 'Author candidate rebuild failed twice (immediate + 3min retry). See logs/nightly.log.'
|
|
142
|
+
}, open('$MARKER', 'w'), indent=2)
|
|
143
|
+
"
|
|
144
|
+
sh scripts/notify.sh "🔴 Author candidate rebuild FAILED twice" "candidate rebuild failed; last release remains $AFTER. Last: $TAIL" urgent "rotating_light"
|
|
145
|
+
echo "===== ESCALATED: marker written at $MARKER =====" >> "$LOG"
|
|
146
|
+
exit 1
|
|
147
|
+
fi
|
|
148
|
+
fi
|
|
149
|
+
|
|
78
150
|
# Memory distillation (ADR-174) had gone stale 3 days — same silent-death pattern as everything
|
|
79
151
|
# else tonight. Independent of rebuild success/failure: mine raw memory_entries into structured
|
|
80
152
|
# episodes/reasoning_patterns on every nightly run. Best-effort, never blocks the real job.
|
|
81
|
-
RUFLO_DAEMON_AUTOSTART=0 ~/.npm-global/bin/ruflo memory distill run --path
|
|
153
|
+
RUFLO_DAEMON_AUTOSTART=0 ~/.npm-global/bin/ruflo memory distill run --path "$RUVNET_PROJECT_MEMORY_DB" >> "$LOG" 2>&1 || true
|
|
82
154
|
# Durability: was a one-off manual snapshot before tonight. Now recurring — WAL-safe, rotates
|
|
83
155
|
# automatically (keeps last 14), zero risk to the live DB (reads only).
|
|
84
|
-
RUFLO_DAEMON_AUTOSTART=0 ~/.npm-global/bin/ruflo memory backup --db
|
|
156
|
+
RUFLO_DAEMON_AUTOSTART=0 ~/.npm-global/bin/ruflo memory backup --db "$RUVNET_PROJECT_MEMORY_DB" --keep 14 >> "$LOG" 2>&1 || true
|
|
85
157
|
|
|
86
158
|
# LESSON BRIDGE (ADR-066, 2026-08-10) — carry newly-tagged machine-wide lessons into the store the
|
|
87
159
|
# hooks actually read. Without this the bridge is a command someone has to remember, and a learning
|
|
@@ -93,7 +165,7 @@ RUFLO_DAEMON_AUTOSTART=0 ~/.npm-global/bin/ruflo memory backup --db .swarm/memor
|
|
|
93
165
|
# it REFUSES to apply when it reads zero rows while bridged lessons are installed — so a transient
|
|
94
166
|
# read failure can never strip the store. Best-effort, never blocks the rebuild.
|
|
95
167
|
echo "===== lesson-bridge — $(date -u +%FT%TZ) =====" >> "$LOG"
|
|
96
|
-
|
|
168
|
+
"$NODE_BIN" plugin/scripts/lesson-bridge.mjs --apply >> "$LOG" 2>&1 \
|
|
97
169
|
|| echo "===== lesson-bridge: SKIPPED (see line above) =====" >> "$LOG"
|
|
98
170
|
|
|
99
171
|
# AgentDB drift canary (2026-07-23, P7 wiring sweep) — scripts/memdb-health.sh existed unwired since
|
|
@@ -101,7 +173,7 @@ echo "===== lesson-bridge — $(date -u +%FT%TZ) =====" >> "$LOG"
|
|
|
101
173
|
# while in WAL mode. Best-effort, same shape as its brain-health/key-health siblings below: never
|
|
102
174
|
# blocks the real rebuild, just makes a WAL-mode/integrity regression loud instead of silent.
|
|
103
175
|
echo "===== memdb-health canary — $(date -u +%FT%TZ) =====" >> "$LOG"
|
|
104
|
-
sh scripts/memdb-health.sh
|
|
176
|
+
sh scripts/memdb-health.sh "$RUVNET_PROJECT_MEMORY_DB" >> "$LOG" 2>&1 \
|
|
105
177
|
&& echo "===== memdb-health canary: OK =====" >> "$LOG" \
|
|
106
178
|
|| echo "===== memdb-health canary: UNHEALTHY (see line above) =====" >> "$LOG"
|
|
107
179
|
|
|
@@ -141,15 +213,15 @@ sh scripts/memdb-health.sh .swarm/memory.db >> "$LOG" 2>&1 \
|
|
|
141
213
|
# REPORTS ONLY. It cannot push, merge, close or publish (asserted by test). Exit 1 means a human
|
|
142
214
|
# needs to look, and the reasons print in full rather than as a count.
|
|
143
215
|
echo "===== GITHUB-HEALTH watch — $(date -u +%FT%TZ) =====" >> "$LOG"
|
|
144
|
-
|
|
216
|
+
"$NODE_BIN" scripts/github-health-watch.mjs >> "$LOG" 2>&1 \
|
|
145
217
|
|| echo "[github-health] findings above need attention" >> "$LOG"
|
|
146
218
|
|
|
147
219
|
echo "===== RELEASE-CONVERGENCE watchdog — $(date -u +%FT%TZ) =====" >> "$LOG"
|
|
148
|
-
|
|
220
|
+
"$NODE_BIN" scripts/release-convergence-watchdog.mjs --dispatch >> "$LOG" 2>&1 \
|
|
149
221
|
|| echo "[release-watchdog] exited non-zero — see above; nightly continues" >> "$LOG"
|
|
150
222
|
|
|
151
223
|
echo "===== LEARNING-REPLAY counterfactual trap — $(date -u +%FT%TZ) =====" >> "$LOG"
|
|
152
|
-
|
|
224
|
+
"$NODE_BIN" scripts/learning-replay.mjs --n 3 --model haiku >> "$LOG" 2>&1
|
|
153
225
|
LR_RC=$?
|
|
154
226
|
case "$LR_RC" in
|
|
155
227
|
0) echo "===== LEARNING-REPLAY: PASS =====" >> "$LOG" ;;
|
|
@@ -165,7 +237,7 @@ esac
|
|
|
165
237
|
BRAIN_KB="$HOME/.cache/ruvnet-brain/kb"
|
|
166
238
|
if [ -f "$BRAIN_KB/forge-ask-all.mjs" ]; then
|
|
167
239
|
echo "===== brain-health canary — $(date -u +%FT%TZ) =====" >> "$LOG"
|
|
168
|
-
if (cd "$BRAIN_KB" &&
|
|
240
|
+
if (cd "$BRAIN_KB" && "$NODE_BIN" forge-ask-all.mjs --dir . --q "HNSW vector index" --k 1) >> "$LOG" 2>&1; then
|
|
169
241
|
echo "===== brain-health canary: OK =====" >> "$LOG"
|
|
170
242
|
else
|
|
171
243
|
echo "===== brain-health canary: DOWN — escalating =====" >> "$LOG"
|
|
@@ -181,39 +253,8 @@ fi
|
|
|
181
253
|
# The canary itself handles urgent pushes on alive->DEAD transitions (and recovery notices), so a
|
|
182
254
|
# known-dead key doesn't re-alarm every night; here we only log.
|
|
183
255
|
echo "===== key-health canary — $(date -u +%FT%TZ) =====" >> "$LOG"
|
|
184
|
-
zsh -lc 'cd
|
|
256
|
+
zsh -lc 'cd "$1" && "$2" scripts/key-canary.mjs --notify' _ "$WORKTREE_ROOT" "$NODE_BIN" >> "$LOG" 2>&1 \
|
|
185
257
|
&& echo "===== key-health canary: all present keys alive =====" >> "$LOG" \
|
|
186
258
|
|| echo "===== key-health canary: at least one key DEAD (push sent on new deaths) =====" >> "$LOG"
|
|
187
259
|
|
|
188
|
-
|
|
189
|
-
exit 0
|
|
190
|
-
fi
|
|
191
|
-
|
|
192
|
-
echo "===== first attempt failed — waiting 3 min, retrying once (self-heal for transient issues) =====" >> "$LOG"
|
|
193
|
-
sleep 180
|
|
194
|
-
|
|
195
|
-
if run_once 2; then
|
|
196
|
-
echo "===== SELF-HEALED on retry — no escalation needed =====" >> "$LOG"
|
|
197
|
-
exit 0
|
|
198
|
-
fi
|
|
199
|
-
|
|
200
|
-
# Both attempts genuinely failed. Escalate loudly AND leave a marker the next session cannot miss.
|
|
201
|
-
AFTER=$(gh release view --json tagName -q .tagName 2>/dev/null || echo "unknown")
|
|
202
|
-
# Was `tail -8 | cut -c1-600`. self-update's [FATAL] block is 4+ lines on its own and now carries a
|
|
203
|
-
# 'reason:' line per failed repo, so 8 lines / 600 chars truncated the alert mid-argv — every one of
|
|
204
|
-
# the six identical 2026-08-03..08-06 failures escalated with no reason in it. Widen enough that the
|
|
205
|
-
# whole [FATAL] block, reasons included, reaches the marker file and the push.
|
|
206
|
-
TAIL=$(tail -25 "$LOG" | tr '\n' ' ' | cut -c1-2000)
|
|
207
|
-
mkdir -p .ruvnet-brain
|
|
208
|
-
python3 -c "
|
|
209
|
-
import json, datetime
|
|
210
|
-
json.dump({
|
|
211
|
-
'at': datetime.datetime.utcnow().isoformat() + 'Z',
|
|
212
|
-
'last_release_tag_observed': '$AFTER',
|
|
213
|
-
'tail': '''$TAIL''',
|
|
214
|
-
'note': 'Nightly failed twice (immediate + 3min retry). Needs a live session to diagnose — see logs/nightly.log.'
|
|
215
|
-
}, open('$MARKER', 'w'), indent=2)
|
|
216
|
-
"
|
|
217
|
-
sh scripts/notify.sh "🔴 Nightly FAILED twice — needs you" "candidate rebuild failed; last release remains $AFTER. Last: $TAIL" urgent "rotating_light"
|
|
218
|
-
echo "===== ESCALATED: marker written at $MARKER =====" >> "$LOG"
|
|
219
|
-
exit 1
|
|
260
|
+
exit 0
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import {
|
|
6
|
+
buildSourceScopeReceipt,
|
|
7
|
+
canonicalJson,
|
|
8
|
+
digest,
|
|
9
|
+
validateSourceScopeReceipt,
|
|
10
|
+
} from './source-scope-receipt.mjs';
|
|
11
|
+
|
|
12
|
+
export const PRODUCT_INTEGRITY_SCHEMA_VERSION = 1;
|
|
13
|
+
export const PRODUCT_INTEGRITY_TEST_CLASSES = Object.freeze(['essential', 'supporting', 'obsolete']);
|
|
14
|
+
export const PRODUCT_INTEGRITY_PROOF_STRENGTH = Object.freeze(['unit', 'integration', 'packed-artifact', 'candidate-host', 'public-byte']);
|
|
15
|
+
export const PRODUCT_INTEGRITY_PROCESSES = Object.freeze([
|
|
16
|
+
{ id: 'SourceCoverage', upstream: [] }, { id: 'CorpusGeneration', upstream: ['SourceCoverage'] },
|
|
17
|
+
{ id: 'ReleaseProjection', upstream: ['CorpusGeneration'] }, { id: 'RefreshLifecycle', upstream: ['ReleaseProjection'] },
|
|
18
|
+
{ id: 'HostConvergence', upstream: ['ReleaseProjection'] }, { id: 'ReleaseTransaction', upstream: ['ReleaseProjection'] },
|
|
19
|
+
{ id: 'PublicVerification', upstream: ['ReleaseTransaction', 'HostConvergence'] },
|
|
20
|
+
{ id: 'ProductIntegrityCase', upstream: ['RefreshLifecycle', 'PublicVerification'] },
|
|
21
|
+
]);
|
|
22
|
+
export const PRODUCT_INTEGRITY_CONTEXTS = Object.freeze(PRODUCT_INTEGRITY_PROCESSES.map(({ id }) => id));
|
|
23
|
+
const ARCHITECTURE = Object.freeze([
|
|
24
|
+
{ id: 'ADR-072', path: 'docs/adr/0072-whole-product-integrity-conformance.md', status: 'Accepted' },
|
|
25
|
+
{ id: 'DDD-0018', path: 'docs/ddd/0018-product-integrity-context.md', status: 'Accepted' },
|
|
26
|
+
{ id: 'ADR-073', path: 'docs/adr/0073-agentdb-perennial-project-continuity.md', status: 'Accepted' },
|
|
27
|
+
{ id: 'DDD-0019', path: 'docs/ddd/0019-project-continuity-context.md', status: 'Accepted' },
|
|
28
|
+
{ id: 'ADR-074', path: 'docs/adr/0074-ruvnet-capability-claim-integrity.md', status: 'Accepted' },
|
|
29
|
+
{ id: 'DDD-0020', path: 'docs/ddd/0020-capability-claim-integrity-context.md', status: 'Accepted' },
|
|
30
|
+
]);
|
|
31
|
+
const proofStrength = (file) => file.startsWith('tests/acceptance/') ? 'packed-artifact'
|
|
32
|
+
: file.startsWith('tests/integration/') ? 'integration' : file.startsWith('tests/qe/') ? 'candidate-host' : 'unit';
|
|
33
|
+
const make = (id, statement, owner, contributors, implementation, tests, receiptKinds, supportingArchitecture = []) => Object.freeze({
|
|
34
|
+
id, statement, owner, contributors: Object.freeze(contributors),
|
|
35
|
+
architecture: Object.freeze(['ADR-072', 'DDD-0018', ...supportingArchitecture]),
|
|
36
|
+
implementation: Object.freeze(implementation), behaviors: Object.freeze([Object.freeze({
|
|
37
|
+
id: `${id}.essential`, class: 'essential', commands: Object.freeze(tests.map((file) => `npx vitest run ${file}`)),
|
|
38
|
+
positive: Object.freeze(tests.map((file) => Object.freeze({ file, strength: proofStrength(file) }))),
|
|
39
|
+
adversarial: Object.freeze(tests.map((file) => Object.freeze({ file, strength: proofStrength(file) }))),
|
|
40
|
+
receiptKinds: Object.freeze(receiptKinds),
|
|
41
|
+
})]),
|
|
42
|
+
});
|
|
43
|
+
export const PRODUCT_INTEGRITY_OBLIGATIONS = Object.freeze([
|
|
44
|
+
make('S-1', 'One complete public corpus', 'CorpusGeneration', ['SourceCoverage'], ['scripts/source-coverage.mjs', 'scripts/corpus-reconcile.mjs', 'scripts/corpus-candidate.mjs'], ['tests/unit/source-coverage.test.mjs', 'tests/unit/corpus-reconcile.test.mjs'], ['ruvnet-brain-source-observation', 'ruvnet-brain-corpus-candidate']),
|
|
45
|
+
make('S-2', 'One immutable public release projection', 'ReleaseProjection', ['CorpusGeneration'], ['scripts/build-bundle.mjs', 'scripts/coverage-integrity.mjs', 'scripts/public-inventory.mjs'], ['tests/integration/build-bundle-fence.test.mjs', 'tests/unit/public-inventory.test.mjs'], ['ruvnet-brain-release-coverage']),
|
|
46
|
+
make('S-3', 'Recall at 10 is at least 98 percent and delta citations are complete', 'PublicVerification', ['SourceCoverage', 'CorpusGeneration'], ['scripts/retrieval-canary.mjs', 'scripts/packed-retrieval-canary.mjs'], ['tests/unit/retrieval-canary.test.mjs', 'tests/unit/packed-retrieval-canary.test.mjs'], ['ruvnet-brain-retrieval-canary-plan', 'ruvnet-brain-retrieval-canary-receipt']),
|
|
47
|
+
make('S-4', 'Native nightly runs are ordered, idempotent, and retained within budget', 'RefreshLifecycle', [], ['bin/nightly-refresh.mjs', 'plugin/scripts/nightly-scheduler.mjs', 'scripts/nightly-two-run-proof.mjs', 'kb/refresh-run.mjs'], ['tests/unit/nightly-scheduler.test.mjs', 'tests/unit/nightly-two-run-proof.test.mjs'], ['ruvnet-brain-refresh-run', 'ruvnet-brain-native-two-run-nightly-proof']),
|
|
48
|
+
make('S-5', 'One active corpus preserves private stores and bounded recovery', 'RefreshLifecycle', ['CorpusGeneration'], ['kb/update-storage-transaction.mjs', 'kb/forge-update.mjs', 'kb/brain-profile.mjs', 'bin/install.mjs'], ['tests/unit/update-storage-transaction.test.mjs', 'tests/unit/brain-profile.test.mjs'], ['ruvnet-brain-update-storage-transaction', 'ruvnet-brain-installed-profile']),
|
|
49
|
+
make('S-6', 'Every supported OS and host loader converges on exact public bytes', 'HostConvergence', ['PublicVerification'], ['scripts/host-registry.mjs', 'scripts/host-install-matrix.mjs', 'scripts/public-verification-aggregate.mjs'], ['tests/unit/host-registry.test.mjs', 'tests/integration/dual-host-install.test.mjs'], ['ruvnet-brain-host-registry', 'ruvnet-brain-public-verification-leaf']),
|
|
50
|
+
make('S-7', 'Publication ends only after signed public verification', 'PublicVerification', ['ReleaseTransaction'], ['scripts/release-transaction.mjs', 'scripts/public-verification-finalizer.mjs', '.github/workflows/protected-release.yml'], ['tests/qe/release/release-transaction-faults.test.mjs', 'tests/unit/public-verification-finalizer.test.mjs'], ['ruvnet-brain-release-transaction-receipt', 'ruvnet-brain-public-verification-aggregate']),
|
|
51
|
+
make('S-8', 'Accepted architecture has one executable owner and source-bound trace', 'ProductIntegrityCase', [], ['scripts/product-integrity-contract.mjs', 'scripts/source-scope-receipt.mjs'], ['tests/unit/product-integrity-contract.test.mjs', 'tests/unit/source-scope-receipt.test.mjs'], ['ruvnet-brain-product-integrity-contract', 'ruvnet-brain-product-integrity-trace']),
|
|
52
|
+
make('S-9', 'Every essential behavior has positive and adversarial proof', 'ProductIntegrityCase', [], ['scripts/product-integrity-contract.mjs', 'scripts/adr-072-completion.mjs'], ['tests/unit/product-integrity-contract.test.mjs', 'tests/unit/adr-072-completion.test.mjs'], ['ruvnet-brain-product-integrity-trace', 'ruvnet-brain-adr-072-completion']),
|
|
53
|
+
make('S-10', 'Fable 5 and GPT-5.6-Sol review identical immutable inputs', 'ProductIntegrityCase', ['PublicVerification'], ['scripts/public-verification-aggregate.mjs', '.github/workflows/protected-release.yml'], ['tests/unit/public-verification-aggregate.test.mjs', 'tests/unit/protected-release-workflow.test.mjs'], ['ruvnet-brain-independent-review', 'ruvnet-brain-public-verification-aggregate']),
|
|
54
|
+
make('S-11', 'Project continuity is complete and host-neutral', 'ProductIntegrityCase', ['HostConvergence', 'RefreshLifecycle'], ['plugin/scripts/project-progression-contract.mjs', 'plugin/scripts/project-progression-hook.mjs'], ['tests/unit/project-progression-contract.test.mjs', 'tests/acceptance/cross-host-project-resume.test.mjs'], ['ruvnet-brain-project-progression', 'ruvnet-brain-cross-host-resume'], ['ADR-073', 'DDD-0019']),
|
|
55
|
+
make('S-12', 'RuvNet capability claims are bound to live evidence before delivery', 'ProductIntegrityCase', ['HostConvergence', 'PublicVerification'], [
|
|
56
|
+
'plugin/scripts/capability-inventory-receipt.mjs',
|
|
57
|
+
'plugin/scripts/capability-claim-evidence.mjs',
|
|
58
|
+
'plugin/scripts/continuation-gate.mjs',
|
|
59
|
+
'plugin/mcp/managed-cli-interface.mjs',
|
|
60
|
+
'kb/forge-evidence.mjs',
|
|
61
|
+
], [
|
|
62
|
+
'tests/unit/capability-inventory-receipt.test.mjs',
|
|
63
|
+
'tests/unit/capability-claim-evidence.test.mjs',
|
|
64
|
+
'tests/unit/continuation-gate-capability-truth.test.mjs',
|
|
65
|
+
'tests/unit/managed-cli-interface.test.mjs',
|
|
66
|
+
'tests/unit/grounding-receipt-lanes.test.mjs',
|
|
67
|
+
'tests/acceptance/adr-074-packed-capability-claims.acceptance.test.mjs',
|
|
68
|
+
], [
|
|
69
|
+
'ruvnet-brain-capability-inventory',
|
|
70
|
+
'ruvnet-brain-capability-claim-audit',
|
|
71
|
+
'ruvnet-brain-source-claim',
|
|
72
|
+
'ruvnet-brain-live-surface',
|
|
73
|
+
'ruvnet-brain-capability-claim-aggregate',
|
|
74
|
+
], ['ADR-074', 'DDD-0020']),
|
|
75
|
+
]);
|
|
76
|
+
const IDS = Object.freeze(Array.from({ length: 12 }, (_, index) => `S-${index + 1}`));
|
|
77
|
+
const safePath = (value) => typeof value === 'string' && value.length > 0 && !path.isAbsolute(value) && !value.split(/[\\/]/).includes('..');
|
|
78
|
+
|
|
79
|
+
function validateProcesses(processes) {
|
|
80
|
+
if (!Array.isArray(processes) || canonicalJson(processes.map(({ id }) => id)) !== canonicalJson(PRODUCT_INTEGRITY_CONTEXTS)) throw new Error('product integrity contract must contain the exact eight processes');
|
|
81
|
+
const ids = new Set(processes.map(({ id }) => id)); const visiting = new Set(); const visited = new Set();
|
|
82
|
+
const visit = (id) => { if (visiting.has(id)) throw new Error('product integrity process graph contains a cycle'); if (visited.has(id)) return;
|
|
83
|
+
visiting.add(id); const row = processes.find((entry) => entry.id === id);
|
|
84
|
+
if (!Array.isArray(row?.upstream) || new Set(row.upstream).size !== row.upstream.length || row.upstream.some((upstream) => !ids.has(upstream) || upstream === id)) throw new Error(`${id} has invalid upstream processes`);
|
|
85
|
+
row.upstream.forEach(visit); visiting.delete(id); visited.add(id); };
|
|
86
|
+
processes.forEach(({ id }) => visit(id));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function validateProductIntegrityContract(input = {}) {
|
|
90
|
+
const processes = Array.isArray(input) ? PRODUCT_INTEGRITY_PROCESSES : input.processes || PRODUCT_INTEGRITY_PROCESSES;
|
|
91
|
+
const obligations = Array.isArray(input) ? input : input.obligations || PRODUCT_INTEGRITY_OBLIGATIONS;
|
|
92
|
+
const architecture = input.architecture || ARCHITECTURE;
|
|
93
|
+
validateProcesses(processes);
|
|
94
|
+
if (!Array.isArray(architecture) || new Set(architecture.map(({ id }) => id)).size !== architecture.length || architecture.some((row) => row.status !== 'Accepted' || !safePath(row.path))) throw new Error('governing architecture is missing or not Accepted');
|
|
95
|
+
if (!Array.isArray(obligations) || canonicalJson(obligations.map(({ id }) => id)) !== canonicalJson(IDS)) throw new Error('product integrity obligations are missing, duplicated, or out of order');
|
|
96
|
+
const contexts = new Set(processes.map(({ id }) => id)); const architectureIds = new Set(architecture.map(({ id }) => id)); const behaviorIds = new Set();
|
|
97
|
+
for (const row of obligations) {
|
|
98
|
+
if (!contexts.has(row.owner)) throw new Error(`${row.id} has no valid sole owner`);
|
|
99
|
+
if (!Array.isArray(row.contributors) || new Set(row.contributors).size !== row.contributors.length || row.contributors.some((context) => !contexts.has(context)) || row.contributors.includes(row.owner)) throw new Error(`${row.id} has an invalid contributor`);
|
|
100
|
+
if (!Array.isArray(row.architecture) || row.architecture.some((id) => !architectureIds.has(id))) throw new Error(`${row.id} has missing architecture`);
|
|
101
|
+
if (!Array.isArray(row.implementation) || !row.implementation.length || row.implementation.some((file) => !safePath(file))) throw new Error(`${row.id} has no implementation`);
|
|
102
|
+
if (!Array.isArray(row.behaviors) || !row.behaviors.length) throw new Error(`${row.id} has no behaviors`);
|
|
103
|
+
for (const behavior of row.behaviors) {
|
|
104
|
+
if (!behavior.id || behaviorIds.has(behavior.id)) throw new Error(`${row.id} has duplicate behavior identity`); behaviorIds.add(behavior.id);
|
|
105
|
+
if (!PRODUCT_INTEGRITY_TEST_CLASSES.includes(behavior.class)) throw new Error(`${behavior.id} has invalid test class`);
|
|
106
|
+
if (behavior.class === 'essential') {
|
|
107
|
+
for (const field of ['commands', 'positive', 'adversarial', 'receiptKinds']) if (!Array.isArray(behavior[field]) || !behavior[field].length) throw new Error(`${behavior.id} has no ${field}`);
|
|
108
|
+
for (const proof of [...behavior.positive, ...behavior.adversarial]) if (!safePath(proof.file) || !PRODUCT_INTEGRITY_PROOF_STRENGTH.includes(proof.strength)) throw new Error(`${behavior.id} has invalid proof`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { schemaVersion: 1, kind: 'ruvnet-brain-product-integrity-contract', architecture,
|
|
113
|
+
processes: processes.map((row) => ({ ...row, owns: obligations.filter(({ owner }) => owner === row.id).map(({ id }) => id), contributes: obligations.filter(({ contributors }) => contributors.includes(row.id)).map(({ id }) => id) })), obligations };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function productIntegrityGovernedPaths(contract = validateProductIntegrityContract()) {
|
|
117
|
+
return [...new Set([...contract.architecture.map(({ path: file }) => file), ...contract.obligations.flatMap(({ implementation, behaviors }) => [...implementation, ...behaviors.flatMap(({ positive, adversarial }) => [...positive, ...adversarial].map(({ file }) => file))])])].sort();
|
|
118
|
+
}
|
|
119
|
+
export function buildProductIntegrityTrace({ root = '.', sourceSha, contract = validateProductIntegrityContract(), inventory } = {}) {
|
|
120
|
+
if (!/^[a-f0-9]{40}$/.test(String(sourceSha || ''))) throw new Error('product integrity trace requires an exact source SHA');
|
|
121
|
+
const validated = validateProductIntegrityContract(contract);
|
|
122
|
+
const unsigned = { schemaVersion: 1, kind: 'ruvnet-brain-product-integrity-trace', sourceSha, contractSha256: digest(validated), sourceScope: buildSourceScopeReceipt({ root, sourceSha, governedPaths: productIntegrityGovernedPaths(validated), inventory }), contract: validated, verdict: 'PASS', untested: [] };
|
|
123
|
+
return { ...unsigned, traceSha256: digest(unsigned) };
|
|
124
|
+
}
|
|
125
|
+
export function validateProductIntegrityTrace(trace, { root = '.', sourceSha = trace?.sourceSha, inventory } = {}) {
|
|
126
|
+
const { traceSha256, ...unsigned } = trace || {};
|
|
127
|
+
if (trace?.schemaVersion !== 1 || trace?.kind !== 'ruvnet-brain-product-integrity-trace' || trace.sourceSha !== sourceSha || trace.verdict !== 'PASS' || canonicalJson(trace.untested) !== '[]' || trace.contractSha256 !== digest(trace.contract) || traceSha256 !== digest(unsigned)) throw new Error('product integrity trace identity is invalid');
|
|
128
|
+
validateSourceScopeReceipt(trace.sourceScope, { root, inventory });
|
|
129
|
+
const rebuilt = buildProductIntegrityTrace({ root, sourceSha, contract: trace.contract, inventory });
|
|
130
|
+
if (canonicalJson(rebuilt) !== canonicalJson(trace)) throw new Error('product integrity trace differs from the exact source');
|
|
131
|
+
return trace;
|
|
132
|
+
}
|
|
133
|
+
export function renderProductIntegrityTraceMarkdown(contract = validateProductIntegrityContract()) {
|
|
134
|
+
const lines = ['# ADR-072 generated traceability', '', '> Generated from `scripts/product-integrity-contract.mjs`; do not hand-edit.', '', '## Processes', '', '| Process | Upstream | Owns | Contributes |', '|---|---|---|---|'];
|
|
135
|
+
for (const row of contract.processes) lines.push(`| ${row.id} | ${row.upstream.join(', ') || '—'} | ${row.owns.join(', ') || '—'} | ${row.contributes.join(', ') || '—'} |`);
|
|
136
|
+
lines.push('', '## Obligations', '', '| ID | Statement | Owner | Contributors |', '|---|---|---|---|');
|
|
137
|
+
for (const row of contract.obligations) lines.push(`| ${row.id} | ${row.statement} | ${row.owner} | ${row.contributors.join(', ') || '—'} |`);
|
|
138
|
+
lines.push('', '## Essential behavior proofs', '', '| Behavior | Positive | Adversarial | Receipts |', '|---|---|---|---|');
|
|
139
|
+
for (const row of contract.obligations) for (const behavior of row.behaviors.filter(({ class: type }) => type === 'essential')) lines.push(`| ${behavior.id} | ${behavior.positive.map(({ file, strength }) => `${file} (${strength})`).join('<br>')} | ${behavior.adversarial.map(({ file, strength }) => `${file} (${strength})`).join('<br>')} | ${behavior.receiptKinds.join('<br>')} |`);
|
|
140
|
+
return `${lines.join('\n')}\n`;
|
|
141
|
+
}
|
|
142
|
+
const valueAfter = (argv, flag) => { const index = argv.indexOf(flag); return index >= 0 ? argv[index + 1] : null; };
|
|
143
|
+
export function runProductIntegrityCli(argv = process.argv.slice(2), io = { stdout: process.stdout }) {
|
|
144
|
+
const known = new Set(['--markdown', '--check-markdown', '--trace', '--source-sha', '--out', '--verify-trace']);
|
|
145
|
+
if (argv.some((arg) => arg.startsWith('--') && !known.has(arg))) throw new Error('unknown product integrity argument');
|
|
146
|
+
if (!argv.length) { io.stdout.write(`${canonicalJson(validateProductIntegrityContract())}\n`); return 0; }
|
|
147
|
+
if (argv.length === 1 && argv[0] === '--markdown') { io.stdout.write(renderProductIntegrityTraceMarkdown()); return 0; }
|
|
148
|
+
const check = valueAfter(argv, '--check-markdown');
|
|
149
|
+
if (check) { if (fs.readFileSync(path.resolve(check), 'utf8') !== renderProductIntegrityTraceMarkdown()) throw new Error('generated traceability Markdown differs'); io.stdout.write('product integrity Markdown: PASS\n'); return 0; }
|
|
150
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const sourceSha = valueAfter(argv, '--source-sha'); const out = valueAfter(argv, '--out');
|
|
151
|
+
if (argv.includes('--trace') && sourceSha && out) { fs.writeFileSync(path.resolve(out), `${canonicalJson(buildProductIntegrityTrace({ root, sourceSha }))}\n`, { flag: 'wx', mode: 0o600 }); return 0; }
|
|
152
|
+
const verify = valueAfter(argv, '--verify-trace');
|
|
153
|
+
if (verify && sourceSha) { const trace = JSON.parse(fs.readFileSync(path.resolve(verify), 'utf8')); validateProductIntegrityTrace(trace, { root, sourceSha }); io.stdout.write(`${canonicalJson({ verdict: 'PASS', traceSha256: trace.traceSha256 })}\n`); return 0; }
|
|
154
|
+
throw new Error('invalid product integrity argument combination');
|
|
155
|
+
}
|
|
156
|
+
if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) { try { process.exitCode = runProductIntegrityCli(); } catch (error) { console.error(`[product-integrity-contract] ${error.message}`); process.exitCode = 1; } }
|