ruvnet-brain 4.2.2-dev → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +15 -14
  2. package/bin/install.mjs +11 -2
  3. package/console/architecture.html +12 -14
  4. package/console/install-mockup.html +3 -3
  5. package/kb/zip-extract.mjs +8 -2
  6. package/package.json +7 -1
  7. package/plugin/.claude-plugin/plugin.json +1 -1
  8. package/plugin/.codex-plugin/plugin.json +1 -1
  9. package/plugin/hooks/codex-hooks.json +1 -1
  10. package/plugin/hooks/hooks.json +10 -0
  11. package/plugin/host-adapters/claude.json +11 -0
  12. package/plugin/host-adapters/codex.json +11 -0
  13. package/plugin/mcp/managed-cli-interface.mjs +77 -10
  14. package/plugin/mcp/server.mjs +2 -1
  15. package/plugin/scripts/anticipate.sh +30 -1
  16. package/plugin/scripts/capability-claim-evidence.mjs +431 -0
  17. package/plugin/scripts/capability-inventory-receipt.mjs +225 -0
  18. package/plugin/scripts/capability-registry.mjs +6 -0
  19. package/plugin/scripts/capability-routing.mjs +70 -0
  20. package/plugin/scripts/continuation-gate.mjs +68 -2
  21. package/plugin/scripts/coverage-integrity.mjs +575 -0
  22. package/plugin/scripts/ground-ruvnet.sh +2 -2
  23. package/plugin/scripts/nightly-controller.mjs +19 -2
  24. package/plugin/scripts/project-progression-contract.mjs +409 -0
  25. package/plugin/scripts/project-progression-hook.mjs +196 -0
  26. package/plugin/scripts/project-progression-outbox.mjs +95 -0
  27. package/plugin/scripts/project-progression-session-start.mjs +183 -0
  28. package/plugin/scripts/project-progression-store.mjs +248 -0
  29. package/plugin/scripts/project-store-resolver.mjs +106 -0
  30. package/plugin/scripts/session-snapshot-hook.mjs +29 -1
  31. package/plugin/scripts/session-start-core.mjs +22 -18
  32. package/plugin/scripts/spend-guard.mjs +6 -1
  33. package/scripts/adr-072-completion.mjs +98 -0
  34. package/scripts/behavioral-l1-l4.mjs +11 -8
  35. package/scripts/brain-score.mjs +7 -2
  36. package/scripts/build-bundle.mjs +82 -6
  37. package/scripts/card-from-source.mjs +32 -6
  38. package/scripts/corpus-aggregates.mjs +194 -0
  39. package/scripts/corpus-candidate.mjs +56 -36
  40. package/scripts/corpus-reconcile.mjs +295 -28
  41. package/scripts/corpus-seed-publish.mjs +3 -3
  42. package/scripts/coverage-integrity.mjs +3 -0
  43. package/scripts/gist-receipts.mjs +183 -0
  44. package/scripts/git-hooks/pre-push +21 -0
  45. package/scripts/host-registry.mjs +118 -0
  46. package/scripts/independent-review-receipt.mjs +499 -0
  47. package/scripts/ingest-gists.mjs +34 -5
  48. package/scripts/ingest-new-repos.mjs +5 -0
  49. package/scripts/metaharness-gate.mjs +58 -0
  50. package/scripts/nightly-gists.sh +3 -3
  51. package/scripts/nightly-wrapper.sh +75 -69
  52. package/scripts/product-integrity-contract.mjs +156 -0
  53. package/scripts/public-inventory.mjs +4 -0
  54. package/scripts/public-verification-aggregate.mjs +342 -0
  55. package/scripts/public-verification-finalizer.mjs +59 -0
  56. package/scripts/public-verification-inputs.mjs +473 -0
  57. package/scripts/public-verification-lane.mjs +244 -0
  58. package/scripts/publication-receipt.mjs +50 -6
  59. package/scripts/rebuild-gists-from-receipts.mjs +50 -5
  60. package/scripts/release-projection.mjs +86 -0
  61. package/scripts/release-transaction-provider.mjs +43 -102
  62. package/scripts/release-transaction.mjs +139 -51
  63. package/scripts/release.mjs +15 -7
  64. package/scripts/restore-local-ingests.mjs +2 -2
  65. package/scripts/retrieval-canary.mjs +533 -0
  66. package/scripts/routing-flywheel.mjs +12 -1
  67. package/scripts/rvf-generation.mjs +9 -3
  68. package/scripts/self-update.mjs +19 -44
  69. package/scripts/source-coverage.mjs +415 -0
  70. package/scripts/source-scope-receipt.mjs +84 -0
  71. package/scripts/sync-census.mjs +0 -0
  72. package/scripts/wired-check.mjs +114 -24
  73. package/scripts/worktree-integrity.mjs +123 -0
@@ -42,6 +42,7 @@ const DRY = argv.includes('--dry-run');
42
42
  // and ship via Release, so nightly CI can keep the INDEX fresh even though it cannot commit vectors.
43
43
  const INDEX_ONLY = argv.includes('--index-only');
44
44
  const INDEX_PATH = path.join(ROOT, 'docs', 'RUV-GISTS.md');
45
+ const FETCH_TIMEOUT_MS = Number(process.env.RUVNET_GISTS_FETCH_TIMEOUT_MS || 30_000);
45
46
 
46
47
  // Markdown/text only. A gist's code files are better read from the repo they land in; prose is the
47
48
  // thing repos don't carry.
@@ -49,8 +50,17 @@ const TEXT_EXT = new Set(['.md', '.markdown', '.txt', '.rst']);
49
50
 
50
51
  /** Authenticated GitHub calls via `gh` — 5000 req/hr instead of 60, and no token handling here. */
51
52
  function gh(endpointArgs) {
52
- const r = spawnSync('gh', endpointArgs, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
53
- if (r.status !== 0) throw new Error(`gh ${endpointArgs.join(' ')} failed: ${(r.stderr || '').trim().slice(0, 200)}`);
53
+ const r = spawnSync('gh', endpointArgs, {
54
+ encoding: 'utf8',
55
+ maxBuffer: 64 * 1024 * 1024,
56
+ timeout: FETCH_TIMEOUT_MS,
57
+ });
58
+ if (r.status !== 0) {
59
+ const detail = r.error?.code === 'ETIMEDOUT' || r.signal
60
+ ? `timed out after ${FETCH_TIMEOUT_MS}ms`
61
+ : (r.stderr || '').trim().slice(0, 200);
62
+ throw new Error(`gh ${endpointArgs.join(' ')} failed: ${detail}`);
63
+ }
54
64
  return r.stdout;
55
65
  }
56
66
 
@@ -187,11 +197,23 @@ async function main() {
187
197
  for (const [i, stub] of gists.entries()) {
188
198
  if (i % 25 === 0) process.stdout.write(`\r fetching ${i}/${gists.length}…`);
189
199
  let g;
190
- try { g = fetchGist(stub.id); } catch { skipped++; continue; }
200
+ try {
201
+ g = fetchGist(stub.id);
202
+ } catch (err) {
203
+ skipped++;
204
+ console.error(`\n fetch failed for gist ${stub.id}: ${err.message}`);
205
+ continue;
206
+ }
191
207
  for (const [fname, f] of Object.entries(g.files || {})) {
192
208
  if (!TEXT_EXT.has(path.extname(fname).toLowerCase())) continue;
193
- const body = f.truncated && f.raw_url ? '' : (f.content || '');
194
- if (!body.trim()) { skipped++; continue; }
209
+ // JSONL readers treat U+2028/U+2029 as physical line separators on some runtimes. Normalize
210
+ // them before serialization so one JSON object always remains one physical passage line.
211
+ const body = (f.truncated && f.raw_url ? '' : (f.content || '')).replace(/[\u2028\u2029]/g, '\n');
212
+ if (!body.trim()) {
213
+ skipped++;
214
+ console.error(`\n empty or truncated text file: ${stub.id}/${fname}`);
215
+ continue;
216
+ }
195
217
  const title = (g.description || fname).replace(/\s+/g, ' ').trim().slice(0, 180) || fname;
196
218
  const head = banner(g, fname);
197
219
  // Banner on EVERY chunk, not just the first. Retrieval returns ONE chunk — if the provenance
@@ -212,6 +234,13 @@ async function main() {
212
234
  }
213
235
  process.stdout.write('\r');
214
236
 
237
+ // A receipt-only or partial corpus is more dangerous than a failed run: its RVF idmap can look
238
+ // healthy while silently omitting live source. Preserve the previous complete corpus and make
239
+ // the nightly job retry instead of publishing incomplete search data.
240
+ if (skipped > 0) {
241
+ throw new Error(`ingest-gists: refusing to write partial corpus (${skipped} fetch/content failures)`);
242
+ }
243
+
215
244
  fs.writeFileSync(path.join(KB, `${NAME}.passages.jsonl`), passages.map((p) => JSON.stringify(p)).join('\n') + '\n');
216
245
  fs.writeFileSync(path.join(KB, `${NAME}.meta.json`), JSON.stringify({
217
246
  model: NAME, dimensions: 0, metric: 'cosine', name: NAME,
@@ -34,6 +34,7 @@ import { execFileSync, spawnSync } from 'node:child_process';
34
34
  import path from 'node:path';
35
35
  import { fileURLToPath } from 'node:url';
36
36
  import { storeRoot, storesAt, darkStores } from '../kb/store-root.mjs';
37
+ import { assertIsolatedMutationWorktree } from './worktree-integrity.mjs';
37
38
 
38
39
  const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
39
40
  const OWNER = process.env.RUVNET_ORG_OWNER || 'ruvnet';
@@ -81,6 +82,10 @@ const isMain = (() => {
81
82
  })();
82
83
 
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
+ }
84
89
  const repos = liveOrgRepos();
85
90
  if (!Array.isArray(repos)) {
86
91
  // A LIST WE COULD NOT FETCH IS NOT AN EMPTY LIST. Reporting "0 new repos" here would read as
@@ -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; }
@@ -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: launchd does not inherit a login shell's environment, which is why the
14
- # sibling agent com.ruvnet.brain-nightly has been dying on `spawnSync gh ENOENT` — gh lives in
15
- # /opt/homebrew/bin and launchd never saw it.
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 — the ONLY thing launchd invokes for the nightly. Per Stuart's standing order
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
  #
@@ -34,22 +38,42 @@ set -u
34
38
  # kb/models-cache (the fallback) starts cold every time -> every nightly re-downloads the ONNX
35
39
  # embedder from HuggingFace. Point at the already-warm cache instead (verified present).
36
40
  export KB_MODEL_CACHE="/Users/stuartkerr/Code/PowerPlatePulse/scripts/models-cache"
37
- cd /Users/stuartkerr/Code/ruvnet-brain 2>/dev/null || {
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 || {
38
44
  curl -sS --max-time 10 -H "Title: 🔴 Nightly CRASHED before it could even start" \
39
45
  -H "Priority: urgent" -H "Tags: rotating_light" \
40
46
  -d "cd into the repo failed — filesystem or mount problem. Investigate the machine directly." \
41
47
  "https://ntfy.sh/$(grep -m1 '^NTFY_TOPIC=' /Users/stuartkerr/Code/ruvnet-brain/.env 2>/dev/null | cut -d= -f2)" >/dev/null 2>&1
42
48
  exit 1
43
49
  }
44
- mkdir -p logs .ruvnet-brain
45
- LOG=logs/nightly.log
46
- MARKER=.ruvnet-brain/nightly-failure.json
47
- LOCK=.ruvnet-brain/nightly.lock
48
-
49
- # Single-instance guard (2026-07-12): the plist has no built-in one. The updater is incremental now,
50
- # but a very large upstream change, a slow model, or a stalled gate can still outlive the window and
51
- # overlap the NEXT scheduled 3:15am fire. A stale lock from a crashed run (PID no longer alive) is
52
- # treated as no lock.
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.
53
77
  if [ -f "$LOCK" ]; then
54
78
  OLD_PID=$(cat "$LOCK" 2>/dev/null)
55
79
  if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then
@@ -61,17 +85,13 @@ if [ -f "$LOCK" ]; then
61
85
  echo "===== $(date -u +%FT%TZ) — stale lock from pid $OLD_PID (not running) — clearing =====" >> "$LOG"
62
86
  fi
63
87
  echo $$ > "$LOCK"
64
- trap 'rm -f "$LOCK"' EXIT
65
-
66
- # NIGHTLY_SMOKE=1 — prove this whole chain (launchd -> heartbeat wrapper -> here -> node -> the repo
67
- # scan -> the log) WITHOUT a multi-hour rebuild. self-update.mjs is dry-run by default, so dropping
68
- # --apply exercises every link except the build itself. Publication is never part of this job.
69
- # WHY IT EXISTS (2026-07-13): the 03:15 fire on 07-12 died before it could even write its own log
70
- # ("/bin/bash: logs/nightly.log: No such file or directory" — relative path, no working directory) and
71
- # NOBODY KNEW, because the only way to test this chain was to wait until 03:15 and hope. A scheduled job
72
- # you can only test by waiting for it is a job you are not testing. Now: `NIGHTLY_SMOKE=1 <the exact
73
- # 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.
74
93
  SMOKE="${NIGHTLY_SMOKE:-0}"
94
+ "$NODE_BIN" scripts/worktree-integrity.mjs "$WORKTREE_ROOT" "nightly author maintenance" >> "$LOG" 2>&1 || exit 2
75
95
 
76
96
  run_once() {
77
97
  local rc
@@ -98,13 +118,42 @@ run_once() {
98
118
  return 0
99
119
  }
100
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
+
101
150
  # Memory distillation (ADR-174) had gone stale 3 days — same silent-death pattern as everything
102
151
  # else tonight. Independent of rebuild success/failure: mine raw memory_entries into structured
103
152
  # episodes/reasoning_patterns on every nightly run. Best-effort, never blocks the real job.
104
- RUFLO_DAEMON_AUTOSTART=0 ~/.npm-global/bin/ruflo memory distill run --path .swarm/memory.db >> "$LOG" 2>&1 || true
153
+ RUFLO_DAEMON_AUTOSTART=0 ~/.npm-global/bin/ruflo memory distill run --path "$RUVNET_PROJECT_MEMORY_DB" >> "$LOG" 2>&1 || true
105
154
  # Durability: was a one-off manual snapshot before tonight. Now recurring — WAL-safe, rotates
106
155
  # automatically (keeps last 14), zero risk to the live DB (reads only).
107
- RUFLO_DAEMON_AUTOSTART=0 ~/.npm-global/bin/ruflo memory backup --db .swarm/memory.db --keep 14 >> "$LOG" 2>&1 || true
156
+ RUFLO_DAEMON_AUTOSTART=0 ~/.npm-global/bin/ruflo memory backup --db "$RUVNET_PROJECT_MEMORY_DB" --keep 14 >> "$LOG" 2>&1 || true
108
157
 
109
158
  # LESSON BRIDGE (ADR-066, 2026-08-10) — carry newly-tagged machine-wide lessons into the store the
110
159
  # hooks actually read. Without this the bridge is a command someone has to remember, and a learning
@@ -124,7 +173,7 @@ echo "===== lesson-bridge — $(date -u +%FT%TZ) =====" >> "$LOG"
124
173
  # while in WAL mode. Best-effort, same shape as its brain-health/key-health siblings below: never
125
174
  # blocks the real rebuild, just makes a WAL-mode/integrity regression loud instead of silent.
126
175
  echo "===== memdb-health canary — $(date -u +%FT%TZ) =====" >> "$LOG"
127
- sh scripts/memdb-health.sh .swarm/memory.db >> "$LOG" 2>&1 \
176
+ sh scripts/memdb-health.sh "$RUVNET_PROJECT_MEMORY_DB" >> "$LOG" 2>&1 \
128
177
  && echo "===== memdb-health canary: OK =====" >> "$LOG" \
129
178
  || echo "===== memdb-health canary: UNHEALTHY (see line above) =====" >> "$LOG"
130
179
 
@@ -164,18 +213,6 @@ sh scripts/memdb-health.sh .swarm/memory.db >> "$LOG" 2>&1 \
164
213
  # REPORTS ONLY. It cannot push, merge, close or publish (asserted by test). Exit 1 means a human
165
214
  # needs to look, and the reasons print in full rather than as a count.
166
215
  echo "===== GITHUB-HEALTH watch — $(date -u +%FT%TZ) =====" >> "$LOG"
167
- # LOAD WHAT rUv SHIPPED, WITHOUT BEING ASKED (added 2026-08-20).
168
- #
169
- # The owner: "aren't you loading everything Ruv creates every day?????" The answer was no — this
170
- # file had ZERO ingestion, so new repos only entered the brain when a human typed the command.
171
- # Measured the day this landed: 181 live repos, 69 ingested, 125 missing. `brain-stamp.mjs` had
172
- # been COMPUTING that gap nightly and nothing acted on it.
173
- #
174
- # Bounded to 3/night on purpose: ingestion embeds a whole repository, and 125 in one run would
175
- # starve everything after it here. The corpus converges over days and the run says how many
176
- # remain rather than implying it finished. A failed repo is left missing so the next night retries.
177
- "$NODE_BIN" scripts/ingest-new-repos.mjs --apply --max 3 >> "$LOG" 2>&1 || true
178
-
179
216
  "$NODE_BIN" scripts/github-health-watch.mjs >> "$LOG" 2>&1 \
180
217
  || echo "[github-health] findings above need attention" >> "$LOG"
181
218
 
@@ -216,39 +253,8 @@ fi
216
253
  # The canary itself handles urgent pushes on alive->DEAD transitions (and recovery notices), so a
217
254
  # known-dead key doesn't re-alarm every night; here we only log.
218
255
  echo "===== key-health canary — $(date -u +%FT%TZ) =====" >> "$LOG"
219
- zsh -lc 'cd /Users/stuartkerr/Code/ruvnet-brain && "$NODE_BIN" scripts/key-canary.mjs --notify' >> "$LOG" 2>&1 \
256
+ zsh -lc 'cd "$1" && "$2" scripts/key-canary.mjs --notify' _ "$WORKTREE_ROOT" "$NODE_BIN" >> "$LOG" 2>&1 \
220
257
  && echo "===== key-health canary: all present keys alive =====" >> "$LOG" \
221
258
  || echo "===== key-health canary: at least one key DEAD (push sent on new deaths) =====" >> "$LOG"
222
259
 
223
- if run_once 1; then
224
- exit 0
225
- fi
226
-
227
- echo "===== first attempt failed — waiting 3 min, retrying once (self-heal for transient issues) =====" >> "$LOG"
228
- sleep 180
229
-
230
- if run_once 2; then
231
- echo "===== SELF-HEALED on retry — no escalation needed =====" >> "$LOG"
232
- exit 0
233
- fi
234
-
235
- # Both attempts genuinely failed. Escalate loudly AND leave a marker the next session cannot miss.
236
- AFTER=$(gh release view --json tagName -q .tagName 2>/dev/null || echo "unknown")
237
- # Was `tail -8 | cut -c1-600`. self-update's [FATAL] block is 4+ lines on its own and now carries a
238
- # 'reason:' line per failed repo, so 8 lines / 600 chars truncated the alert mid-argv — every one of
239
- # the six identical 2026-08-03..08-06 failures escalated with no reason in it. Widen enough that the
240
- # whole [FATAL] block, reasons included, reaches the marker file and the push.
241
- TAIL=$(tail -25 "$LOG" | tr '\n' ' ' | cut -c1-2000)
242
- mkdir -p .ruvnet-brain
243
- python3 -c "
244
- import json, datetime
245
- json.dump({
246
- 'at': datetime.datetime.utcnow().isoformat() + 'Z',
247
- 'last_release_tag_observed': '$AFTER',
248
- 'tail': '''$TAIL''',
249
- 'note': 'Nightly failed twice (immediate + 3min retry). Needs a live session to diagnose — see logs/nightly.log.'
250
- }, open('$MARKER', 'w'), indent=2)
251
- "
252
- sh scripts/notify.sh "🔴 Nightly FAILED twice — needs you" "candidate rebuild failed; last release remains $AFTER. Last: $TAIL" urgent "rotating_light"
253
- echo "===== ESCALATED: marker written at $MARKER =====" >> "$LOG"
254
- 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; } }
@@ -0,0 +1,4 @@
1
+ // One validator for build, archive, install, update, Console, and public proof.
2
+ // The implementation ships with the plugin so installed activation boundaries execute the exact
3
+ // same byte/inventory checks as release assembly.
4
+ export { validatePublicInventory } from '../plugin/scripts/coverage-integrity.mjs';