ruvnet-brain 4.0.1 → 4.0.2
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/.claude-plugin/marketplace.json +1 -0
- package/README.md +4 -4
- package/bin/install.mjs +100 -5
- package/console/CONTRACT.md +172 -0
- package/console/activity.js +753 -0
- package/console/app.js +4189 -0
- package/console/architecture.html +1221 -0
- package/console/assets/depth-1.webp +0 -0
- package/console/assets/depth-2.webp +0 -0
- package/console/assets/depth-3.webp +0 -0
- package/console/assets/harness-vs-plain.svg +259 -0
- package/console/assets/hero.webp +0 -0
- package/console/assets/memory.webp +0 -0
- package/console/assets/metaharness.svg +247 -0
- package/console/index.html +777 -0
- package/console/install-architecture.html +162 -0
- package/console/install-mockup.html +543 -0
- package/console/style.css +2144 -0
- package/console/tips.css +926 -0
- package/console/tips.html +858 -0
- package/console/tips.js +128 -0
- package/docs/RELEASE-NOTES-4.0.md +88 -0
- package/kb/model-requirements.mjs +37 -6
- package/keys/ruvnet-brain-signing.pub.pem +3 -0
- package/package.json +8 -22
- package/plugin/.claude-plugin/marketplace.json +1 -0
- package/plugin/.claude-plugin/plugin.json +2 -3
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/brain-console.md +2 -2
- package/plugin/commands/configure.md +3 -2
- package/plugin/commands/rvbc.md +4 -3
- package/plugin/commands/rvcb.md +2 -2
- package/plugin/hooks/hooks.json +1 -2
- package/plugin/mcp/managed-cli-interface.mjs +47 -4
- package/plugin/mcp/server.mjs +21 -0
- package/plugin/scripts/detach.mjs +14 -0
- package/plugin/scripts/first-session-worker.mjs +38 -0
- package/plugin/scripts/ground-ruvnet.sh +16 -6
- package/plugin/scripts/hook-shim.mjs +7 -7
- package/plugin/scripts/learn-capture.sh +22 -3
- package/plugin/scripts/learn-flush.mjs +21 -4
- package/plugin/scripts/runtime-preferences.mjs +269 -0
- package/plugin/scripts/session-start-core.mjs +477 -0
- package/plugin/scripts/session-start.sh +3 -858
- package/plugin/skills/brain-console/SKILL.md +4 -2
- package/plugin/skills/release-proof/SKILL.md +81 -0
- package/plugin/skills/release-proof/agents/openai.yaml +4 -0
- package/plugin/skills/release-proof/references/receipt-contract.md +38 -0
- package/plugin/skills/release-proof/scripts/release-proof.mjs +210 -0
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +5 -1
- package/plugin/skills/rvbc/SKILL.md +9 -6
- package/scripts/adr-backfill.mjs +107 -0
- package/scripts/advocacy-outcomes.mjs +808 -0
- package/scripts/agentdb-context.mjs +216 -0
- package/scripts/agentdb-fleet-doctor.mjs +101 -0
- package/scripts/ascii-drift.mjs +236 -0
- package/scripts/behavioral-l1-l4.mjs +210 -0
- package/scripts/brain-capability-check.mjs +72 -0
- package/scripts/brain-grade-groundtruth.mjs +100 -0
- package/scripts/brain-latency-50.mjs +227 -0
- package/scripts/brain-novice-50.mjs +189 -0
- package/scripts/brain-stamp.mjs +94 -0
- package/scripts/brain-state.mjs +212 -0
- package/scripts/build-bundle.mjs +522 -0
- package/scripts/build-concepts.mjs +132 -0
- package/scripts/build-l2.mjs +71 -0
- package/scripts/build-primer.mjs +73 -0
- package/scripts/build-symbols.mjs +68 -0
- package/scripts/calibrate-router.mjs +97 -0
- package/scripts/capability-audit.mjs +321 -0
- package/scripts/capability-registry.mjs +876 -0
- package/scripts/check-indexation.mjs +108 -0
- package/scripts/check-legibility.mjs +189 -0
- package/scripts/ci/build-fixture-kb.mjs +67 -0
- package/scripts/ci/learning-replay-codex-adapter.mjs +62 -0
- package/scripts/ci/learning-replay-recorder.mjs +59 -0
- package/scripts/ci/mutate-hook-timeout.mjs +70 -0
- package/scripts/ci/stranger-fixture-stage.mjs +17 -0
- package/scripts/ci/stranger-scenario.mjs +228 -0
- package/scripts/ci/stranger-timeout.mjs +25 -0
- package/scripts/ci-verdict.mjs +29 -0
- package/scripts/claims-verify.mjs +710 -0
- package/scripts/clear-claude-tmp.sh +31 -0
- package/scripts/console-engine.mjs +434 -0
- package/scripts/console-engine.test.mjs +125 -0
- package/scripts/corpus-qa.mjs +250 -0
- package/scripts/correction-detect-embed.mjs +346 -0
- package/scripts/correction-detect-measure.mjs +270 -0
- package/scripts/correction-detect.mjs +686 -0
- package/scripts/count-chunks.mjs +54 -0
- package/scripts/described-questions.json +30 -0
- package/scripts/design-grade.mjs +58 -0
- package/scripts/dev-plugin-link.sh +105 -0
- package/scripts/distill-project.mjs +200 -0
- package/scripts/doc-currency.mjs +801 -0
- package/scripts/eval-brain.mjs +244 -0
- package/scripts/fix-metaharness-memretrieve.mjs +121 -0
- package/scripts/full-hints.mjs +87 -0
- package/scripts/gate.sh +39 -0
- package/scripts/gates.mjs +146 -0
- package/scripts/gen-console-images.mjs +54 -0
- package/scripts/gen-images.mjs +47 -0
- package/scripts/git-clone-refresh.mjs +52 -0
- package/scripts/git-hooks/pre-push +126 -0
- package/scripts/goal-match.mjs +398 -0
- package/scripts/goldie-research.mjs +223 -0
- package/scripts/goldie-weekly.sh +67 -0
- package/scripts/health-repair.mjs +250 -0
- package/scripts/helix-scenario-questions.json +10 -0
- package/scripts/ingest-gists.mjs +230 -0
- package/scripts/ingest-meeting.mjs +115 -0
- package/scripts/ingest-repo.mjs +79 -0
- package/scripts/install-npx-witness.sh +49 -0
- package/scripts/issue-fix.mjs +639 -0
- package/scripts/issue-watch.mjs +276 -0
- package/scripts/issue4-close-note.md +31 -0
- package/scripts/key-canary.mjs +91 -0
- package/scripts/latency-to-surface.mjs +233 -0
- package/scripts/learning-enable.mjs +380 -0
- package/scripts/learning-replay.mjs +1570 -0
- package/scripts/learnings.mjs +62 -0
- package/scripts/lesson-gate.mjs +680 -0
- package/scripts/lesson-lifecycle.mjs +449 -0
- package/scripts/lesson-promote.mjs +262 -0
- package/scripts/lesson-ratify.mjs +98 -0
- package/scripts/lesson-seed.mjs +252 -0
- package/scripts/lesson-store.mjs +447 -0
- package/scripts/loop-checkpoint.mjs +86 -0
- package/scripts/memdb-health.sh +14 -0
- package/scripts/memory-doctor.mjs +271 -0
- package/scripts/model-catalog.mjs +79 -0
- package/scripts/nightly-controller.mjs +66 -0
- package/scripts/nightly-gists.sh +72 -0
- package/scripts/nightly-wrapper.sh +180 -0
- package/scripts/notify.sh +12 -0
- package/scripts/npx-witness.sh +56 -0
- package/scripts/onboarding-console.mjs +2749 -0
- package/scripts/private-fence.mjs +69 -0
- package/scripts/proactivity-metrics.mjs +118 -0
- package/scripts/proof-questions.json +56 -0
- package/scripts/prove.mjs +95 -0
- package/scripts/proxy/claude-proxied.sh +57 -0
- package/scripts/proxy/proxy-revert.sh +59 -0
- package/scripts/proxy/proxy-up.sh +60 -0
- package/scripts/proxy/proxy-verify.mjs +142 -0
- package/scripts/published-surface-probe.mjs +241 -0
- package/scripts/qe/card-lane-gate.mjs +162 -0
- package/scripts/qe/session-start-gate.mjs +229 -0
- package/scripts/qe/ux-suite.mjs +323 -0
- package/scripts/reconcile-project.mjs +0 -0
- package/scripts/record-lesson.mjs +113 -0
- package/scripts/refresh-model-catalog.mjs +99 -0
- package/scripts/release-proof.mjs +9 -0
- package/scripts/release-vector.mjs +281 -0
- package/scripts/release.mjs +395 -0
- package/scripts/remedy-registry.mjs +247 -0
- package/scripts/rerank-cap-eval.mjs +265 -0
- package/scripts/rerank-cap-warm-ab.mjs +129 -0
- package/scripts/route-cheap.mjs +20 -15
- package/scripts/router-utilization.mjs +182 -0
- package/scripts/routing-flywheel.mjs +596 -0
- package/scripts/rvf-generation.mjs +104 -0
- package/scripts/rvf-index-audit.mjs +138 -0
- package/scripts/self-update.mjs +508 -0
- package/scripts/selfcheck.mjs +7 -1
- package/scripts/sign-bundle.mjs +69 -0
- package/scripts/signal-watch.mjs +171 -0
- package/scripts/stack-sync.mjs +469 -0
- package/scripts/stamp-existing-rvf-generations.mjs +53 -0
- package/scripts/stamp-sweep.mjs +144 -0
- package/scripts/status-honesty.mjs +102 -0
- package/scripts/sync-version.mjs +217 -0
- package/scripts/token-report.mjs +102 -0
- package/scripts/top100-benchmark.mjs +479 -0
- package/scripts/top100-corpus.mjs +112 -0
- package/scripts/top100-semantic-assertions.mjs +449 -0
- package/scripts/update-apply.mjs +9 -0
- package/scripts/upgrade-notice.mjs +14 -0
- package/scripts/verify-bundle.mjs +51 -0
- package/scripts/verify-channels.mjs +184 -0
- package/scripts/verify-model-catalog.mjs +104 -0
- package/scripts/verify-nightly-close-issue4.sh +31 -0
- package/scripts/version.mjs +40 -0
- package/scripts/wired-check.mjs +864 -0
- package/plugin/scripts/finalize-token-meter.mjs +0 -25
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// memory-doctor.mjs — does this project's AgentDB actually LEARN, or does it just record?
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS (2026-07-14). For two days I told Stuart "AgentDB is fixed / verified / 33 of 33."
|
|
5
|
+
// It was never fixed, and I never lied — I ran a check that COULD NOT FAIL:
|
|
6
|
+
// ruflo memory store -> ruflo memory search -> got a row back -> "healthy"
|
|
7
|
+
// That exercises `memory_entries`, a key-value table with an HNSW index. It says NOTHING about
|
|
8
|
+
// whether anything is embedded, distilled, or learned. It would pass on a database with the entire
|
|
9
|
+
// intelligence substrate surgically removed — which is exactly the database most of his projects have.
|
|
10
|
+
//
|
|
11
|
+
// LIVENESS IS NOT HEALTH. This file is the referee that makes that distinction impossible to fudge.
|
|
12
|
+
//
|
|
13
|
+
// THE ROOT CAUSE IT FOUND (measured, not theorised):
|
|
14
|
+
// memory_entries.embedding is NULL for ~99% of rows in most projects.
|
|
15
|
+
// ruvnet-brain 1,023 entries · 99.8% embedded -> 456 patterns (learns)
|
|
16
|
+
// ugo-ai-register-now 7,214 entries · 100% embedded -> 3,610 patterns (learns)
|
|
17
|
+
// AMBUILANCE_INVENTORY 11,133 entries · 0.03% embedded -> 1 pattern (dead)
|
|
18
|
+
// flighttest 19,108 entries · 0.3% embedded -> 12 patterns (dead)
|
|
19
|
+
// No embedding -> no semantic recall AND nothing for distill to consume (ADR-174 explicitly skips
|
|
20
|
+
// rows with no parseable vector) -> no patterns -> no episodes -> no intelligence. The whole chain
|
|
21
|
+
// snaps at the first link.
|
|
22
|
+
//
|
|
23
|
+
// And the rows are unembedded because of WHO WRITES THEM: namespaces `hooks:pre-bash`,
|
|
24
|
+
// `hooks:post-bash`, `command-history`, `command-results`, `performance-metrics` — telemetry
|
|
25
|
+
// emitted by the `npx @claude-flow/cli hooks pre-command/post-command` calls wired into ~190 hooks
|
|
26
|
+
// across 16 projects. Five unembedded rows per Bash command. They bury the real memories under
|
|
27
|
+
// thousands of rows of "I ran a command", none of it recallable or distillable.
|
|
28
|
+
//
|
|
29
|
+
// GROUNDED IN rUv's SOURCE (not invented here):
|
|
30
|
+
// ruflo/v3/docs/adr/ADR-174-memory-distillation-self-optimization.md (ACCEPTED) — the
|
|
31
|
+
// RETRIEVE->JUDGE->DISTILL->CONSOLIDATE pipeline, and the finding that the substrate was empty
|
|
32
|
+
// because the consolidate worker was a stub. rUv hit this exact wall and shipped `memory distill`.
|
|
33
|
+
// agentdb/src/controllers/ReflexionMemory.ts — storeEpisode()/retrieveRelevant()/getCritiqueSummary().
|
|
34
|
+
// Its retrieval INNER-JOINs episode_embeddings, which is empty everywhere, so reflexion recall
|
|
35
|
+
// currently returns nothing on every project. That is a SEPARATE layer from ADR-174 distillation;
|
|
36
|
+
// do not conflate them (I did, and had to retract it).
|
|
37
|
+
//
|
|
38
|
+
// This tool DIAGNOSES ONLY. It opens every database read-only and writes nothing, anywhere.
|
|
39
|
+
|
|
40
|
+
import { execFileSync } from 'node:child_process';
|
|
41
|
+
import fs from 'node:fs';
|
|
42
|
+
import os from 'node:os';
|
|
43
|
+
import path from 'node:path';
|
|
44
|
+
|
|
45
|
+
const HOME = os.homedir();
|
|
46
|
+
|
|
47
|
+
// Telemetry namespaces: high-volume, unembedded, zero-signal. Written by the npx hook calls.
|
|
48
|
+
// Counted separately so "you have 11,000 memories" is never mistaken for "you have 11,000 lessons".
|
|
49
|
+
const NOISE_NS = new Set([
|
|
50
|
+
'hooks:pre-bash', 'hooks:post-bash', 'hooks:pre-edit', 'hooks:post-edit',
|
|
51
|
+
'command-history', 'command-results', 'performance-metrics', 'notifications',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
// A doctor that cannot tell "the patient is dead" from "I could not find the patient" is worse than
|
|
55
|
+
// no doctor. The FIRST version of this function swallowed every sqlite error and returned 0 — so a
|
|
56
|
+
// database it could not even OPEN (spaces in the path broke the file: URI) was reported as
|
|
57
|
+
// "0 memories, 0 patterns", indistinguishable from a genuinely empty store. That is the exact
|
|
58
|
+
// can't-fail check this whole file exists to abolish, reproduced inside it. Caught by running it.
|
|
59
|
+
//
|
|
60
|
+
// Now: every query returns {ok, value} or {ok:false, err}. Unreadable is UNKNOWN, never zero, and
|
|
61
|
+
// UNKNOWN is reported loudly rather than averaged into a reassuring number.
|
|
62
|
+
// `mode=ro` ALONE CANNOT OPEN A RESTING WAL DATABASE, and that is not an edge case — it is the normal
|
|
63
|
+
// state of every store nobody is currently using. A WAL database needs its -shm shared-memory segment
|
|
64
|
+
// to be read, and a read-only connection is not allowed to create one, so SQLite returns CANTOPEN(14).
|
|
65
|
+
//
|
|
66
|
+
// MEASURED (fresh WAL db, same file, three ways):
|
|
67
|
+
// sidecars removed + mode=ro -> Error: unable to open database file (14)
|
|
68
|
+
// sidecars present + mode=ro -> 1
|
|
69
|
+
// sidecars removed + mode=ro&immutable=1 -> 1, and `ls` shows no -wal/-shm created
|
|
70
|
+
//
|
|
71
|
+
// This was live on the owner's own machine while it shipped: `.swarm/memory.db-{shm,wal}` had been
|
|
72
|
+
// renamed `.CORRUPT-20260714-144824`, so every single read of the repo's 16MB store returned
|
|
73
|
+
// unreadable, and the console reported memory distillation as permanently UNKNOWN on a healthy store.
|
|
74
|
+
// Three retries two seconds apart cannot fix a structural refusal; they just make it slow.
|
|
75
|
+
//
|
|
76
|
+
// `immutable=1` is GATED, because it promises SQLite the file will not change underneath it — a
|
|
77
|
+
// promise nobody can keep about a database with a live writer, and breaking it returns torn or
|
|
78
|
+
// stale rows rather than an error. So it is used ONLY when both sidecars are absent, which is the
|
|
79
|
+
// observable signature of "no process has this open in WAL mode", and the absence is re-checked
|
|
80
|
+
// AFTER the read so a writer that arrived mid-flight invalidates the result instead of being
|
|
81
|
+
// reported as fact. A locked store still yields an honest unreadable.
|
|
82
|
+
const walSidecarsPresent = (db) => fs.existsSync(`${db}-wal`) || fs.existsSync(`${db}-shm`);
|
|
83
|
+
|
|
84
|
+
const q = (db, sql) => {
|
|
85
|
+
// encodeURI, not raw interpolation: "Helix - Personal Health Intelligence Platform" has spaces,
|
|
86
|
+
// and sqlite3 rejects the URI outright (error 14) rather than falling back to a plain path.
|
|
87
|
+
const base = `file:${encodeURI(db)}?mode=ro`; // read-only: this process will never be a second writer
|
|
88
|
+
const run = (uri) => execFileSync('sqlite3', [uri, sql], { encoding: 'utf8', timeout: 20000, stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
89
|
+
|
|
90
|
+
// MODE IS CHOSEN BEFORE THE OPEN, FROM A FACT ON DISK — NOT AFTER CATCHING WHICHEVER ERROR A
|
|
91
|
+
// GIVEN SQLITE BUILD HAPPENS TO THROW. This used to try plain `mode=ro` first and only add
|
|
92
|
+
// `immutable=1` after catching "unable to open database file (14)". CONFIRMED LIVE 2026-07-26
|
|
93
|
+
// (Docker ubuntu:24.04, apt sqlite3 3.45.1 — the exact version this repo's "check" CI job
|
|
94
|
+
// installs): on a resting WAL db with no sidecars, plain `mode=ro` does NOT throw there the way
|
|
95
|
+
// macOS's bundled 3.51.0 does — it succeeds AND silently vivifies `-shm`/`-wal` as a side effect
|
|
96
|
+
// of establishing WAL-index shared memory for the reader. No exception meant the catch block's
|
|
97
|
+
// sidecar check — the only place this was ever guarded — never ran, so a "read-only" diagnosis
|
|
98
|
+
// left litter in the caller's directory on every ubuntu CI run.
|
|
99
|
+
//
|
|
100
|
+
// The fix does not depend on which behavior a given SQLite build has. It decides the mode from a
|
|
101
|
+
// fact available before either would be attempted: do the sidecars already exist?
|
|
102
|
+
// - present -> a live/resting WAL with real frames; open plain `mode=ro` and let a genuine
|
|
103
|
+
// pending WAL be read normally (immutable=1 here would be a promise we cannot make).
|
|
104
|
+
// - absent -> nothing to replay; open `mode=ro&immutable=1` STRAIGHT AWAY — the one mode both
|
|
105
|
+
// measured SQLite builds leave clean. Plain mode=ro never gets the chance to vivify a -shm
|
|
106
|
+
// file nobody asked for.
|
|
107
|
+
const restingWal = fs.existsSync(db) && !walSidecarsPresent(db);
|
|
108
|
+
const uri = restingWal ? `${base}&immutable=1` : base;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const value = run(uri);
|
|
112
|
+
// Belt and suspenders for a third SQLite build we have not measured: a read that started with
|
|
113
|
+
// no sidecars must end with no sidecars. If one appeared anyway — a fresh concurrent writer, or
|
|
114
|
+
// another version-specific quirk — the bytes just read cannot be trusted, and this read's own
|
|
115
|
+
// litter is removed rather than left in the caller's directory.
|
|
116
|
+
if (restingWal && walSidecarsPresent(db)) {
|
|
117
|
+
for (const suffix of ['-wal', '-shm']) {
|
|
118
|
+
try { fs.unlinkSync(`${db}${suffix}`); } catch { /* best-effort cleanup only */ }
|
|
119
|
+
}
|
|
120
|
+
return { ok: false, err: 'a writer opened the store mid-read' };
|
|
121
|
+
}
|
|
122
|
+
return { ok: true, value, viaImmutable: restingWal || undefined };
|
|
123
|
+
} catch (e) {
|
|
124
|
+
const err = String(e.stderr || e.message || '');
|
|
125
|
+
// "no such table/column" is SCHEMA VARIANCE (an older store) — a real, reportable fact.
|
|
126
|
+
if (/no such (table|column)/.test(err)) return { ok: true, value: null, missing: true };
|
|
127
|
+
// Anything else means we could not read the database, and we must say so, not guess zero.
|
|
128
|
+
return { ok: false, err: err.split('\n')[0].slice(0, 60) };
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
// n() returns null for "unknown" and a number only when we genuinely counted. Callers must handle null.
|
|
132
|
+
const n = (r) => (r.ok ? (r.value === null || r.value === '' ? null : parseInt(r.value, 10)) : null);
|
|
133
|
+
|
|
134
|
+
export function findStores(root = path.join(HOME, 'Code')) {
|
|
135
|
+
const out = [];
|
|
136
|
+
const walk = (dir, depth) => {
|
|
137
|
+
if (depth > 4) return;
|
|
138
|
+
let entries;
|
|
139
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
140
|
+
for (const e of entries) {
|
|
141
|
+
if (!e.isDirectory()) continue;
|
|
142
|
+
if (e.name === 'node_modules' || e.name === '.git') continue;
|
|
143
|
+
if (e.name === '.swarm') {
|
|
144
|
+
const db = path.join(dir, '.swarm/memory.db');
|
|
145
|
+
if (fs.existsSync(db)) out.push(db);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (e.name.startsWith('.') && e.name !== '.swarm') continue;
|
|
149
|
+
walk(path.join(dir, e.name), depth + 1);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
walk(root, 0);
|
|
153
|
+
// Stores outside ~/Code exist too (~/.claude, ~/cognitum-trader). A one-level glob missed 32 of
|
|
154
|
+
// these — including Helix — and that miss is exactly how "33/33 verified" got reported.
|
|
155
|
+
for (const extra of [path.join(HOME, '.claude/.swarm/memory.db'), path.join(HOME, 'cognitum-trader/.swarm/memory.db')]) {
|
|
156
|
+
if (fs.existsSync(extra) && !out.includes(extra)) out.push(extra);
|
|
157
|
+
}
|
|
158
|
+
return out.sort();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function diagnose(db) {
|
|
162
|
+
const name = db.replace(HOME + '/Code/', '').replace(HOME + '/', '~/').replace('/.swarm/memory.db', '');
|
|
163
|
+
|
|
164
|
+
const ic = q(db, 'PRAGMA integrity_check;');
|
|
165
|
+
if (!ic.ok) {
|
|
166
|
+
// UNREADABLE is its own verdict. It is NOT "empty". Reporting it as zero was the bug.
|
|
167
|
+
return { name, db, unreadable: ic.err, findings: [`UNREADABLE: ${ic.err}`], learns: false };
|
|
168
|
+
}
|
|
169
|
+
const integrity = (ic.value || '').split('\n')[0] || 'unknown';
|
|
170
|
+
|
|
171
|
+
const total = n(q(db, 'SELECT count(*) FROM memory_entries;'));
|
|
172
|
+
const embedded = n(q(db, "SELECT count(*) FROM memory_entries WHERE embedding IS NOT NULL AND length(embedding)>0;"));
|
|
173
|
+
const nsRes = q(db, 'SELECT namespace, count(*) FROM memory_entries GROUP BY namespace;');
|
|
174
|
+
const nsRows = (nsRes.value || '').split('\n').filter(Boolean).map((l) => l.split('|'));
|
|
175
|
+
const noise = nsRows.filter(([ns]) => NOISE_NS.has(ns)).reduce((a, [, c]) => a + (parseInt(c, 10) || 0), 0);
|
|
176
|
+
const real = total === null ? null : total - noise;
|
|
177
|
+
|
|
178
|
+
const patterns = n(q(db, 'SELECT count(*) FROM reasoning_patterns;'));
|
|
179
|
+
const patternEmb = n(q(db, 'SELECT count(*) FROM pattern_embeddings;'));
|
|
180
|
+
const episodes = n(q(db, 'SELECT count(*) FROM episodes;'));
|
|
181
|
+
const epEmb = n(q(db, 'SELECT count(*) FROM episode_embeddings;'));
|
|
182
|
+
const critiques = n(q(db, 'SELECT count(critique) FROM episodes;'));
|
|
183
|
+
const skills = n(q(db, 'SELECT count(*) FROM skills;'));
|
|
184
|
+
const promoted = n(q(db, 'SELECT count(*) FROM reasoning_patterns WHERE promoted=1;')); // absent in older schemas -> null
|
|
185
|
+
|
|
186
|
+
// A store with no memory_entries table at all is a DIFFERENT thing from an empty one. Say which.
|
|
187
|
+
if (total === null) {
|
|
188
|
+
return { name, db, integrity, schemaless: true,
|
|
189
|
+
findings: ['no memory_entries table — pre-AgentDB schema, never initialised'], learns: false };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const cover = total ? embedded / total : 0;
|
|
193
|
+
const distilled = real ? patterns / real : 0;
|
|
194
|
+
|
|
195
|
+
// Each finding names the ONE thing that is false, in the order the chain breaks. A doctor that
|
|
196
|
+
// lists twelve symptoms teaches nothing; the first broken link is the only one worth fixing today.
|
|
197
|
+
const findings = [];
|
|
198
|
+
if (integrity !== 'ok') findings.push(`CORRUPT (${integrity})`);
|
|
199
|
+
if (total === 0) findings.push('no memories at all');
|
|
200
|
+
else if (cover < 0.5) findings.push(`only ${(cover * 100).toFixed(1)}% embedded — nothing can be recalled or distilled`);
|
|
201
|
+
else if (patterns === 0) findings.push('embedded but never distilled — run: ruflo memory distill run');
|
|
202
|
+
else if (distilled < 0.05) findings.push(`${patterns} patterns from ${real} real memories — distill barely ran`);
|
|
203
|
+
if (noise > real && noise > 500) findings.push(`${noise} telemetry rows from npx hooks bury ${real} real memories`);
|
|
204
|
+
if (episodes > 0 && epEmb === 0) findings.push('reflexion recall dead (0 episode_embeddings — INNER JOIN returns nothing)');
|
|
205
|
+
if (episodes > 0 && critiques === 0) findings.push('0 critiques — episodes carry no lessons');
|
|
206
|
+
|
|
207
|
+
const learns = cover >= 0.5 && patterns > 0 && distilled >= 0.05;
|
|
208
|
+
return { name, db, integrity, total, embedded, cover, noise, real, patterns, patternEmb,
|
|
209
|
+
episodes, epEmb, critiques, skills, promoted, distilled, findings, learns };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Does this project wire `npx <claude-flow|ruvector>` into its tool-use hooks? Those hooks write
|
|
213
|
+
// telemetry rows (hooks:pre-bash, command-history, ...) with no embedding. THE PREDICTION THIS TESTS:
|
|
214
|
+
// if they are the cause of dead memory, then hooked projects should be dead and unhooked ones alive.
|
|
215
|
+
// If a single project breaks that correlation, the theory is wrong and must be discarded.
|
|
216
|
+
export function hasNpxHooks(db) {
|
|
217
|
+
const projDir = path.dirname(path.dirname(db));
|
|
218
|
+
for (const f of ['.claude/settings.json', '.claude/settings.local.json']) {
|
|
219
|
+
const p = path.join(projDir, f);
|
|
220
|
+
if (!fs.existsSync(p)) continue;
|
|
221
|
+
try {
|
|
222
|
+
const s = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
223
|
+
const hooks = JSON.stringify(s.hooks || {});
|
|
224
|
+
if (/npx\s+(-y\s+)?(@?claude-flow|ruvector|aqe)/.test(hooks)) return true;
|
|
225
|
+
} catch { /* unparseable settings — cannot claim either way */ }
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (process.argv[1] && path.resolve(process.argv[1]).endsWith('memory-doctor.mjs')) {
|
|
231
|
+
const stores = findStores();
|
|
232
|
+
const rows = stores.map((db) => ({ ...diagnose(db), npxHooks: hasNpxHooks(db) }));
|
|
233
|
+
const w = Math.min(34, Math.max(...rows.map((r) => r.name.length), 8));
|
|
234
|
+
const readable = rows.filter((r) => !r.unreadable && !r.schemaless);
|
|
235
|
+
|
|
236
|
+
console.log(`\n AgentDB fleet — ${rows.length} stores found\n`);
|
|
237
|
+
console.log(` ${'PROJECT'.padEnd(w)} ${'MEMORIES'.padStart(9)} ${'EMBED%'.padStart(7)} ${'NOISE'.padStart(7)} ${'PATTERNS'.padStart(8)} ${'npx?'.padStart(5)} LEARNS?`);
|
|
238
|
+
console.log(` ${'-'.repeat(w)} ${'-'.repeat(9)} ${'-'.repeat(7)} ${'-'.repeat(7)} ${'-'.repeat(8)} ${'-'.repeat(5)} -------`);
|
|
239
|
+
for (const r of readable.sort((a, b) => b.total - a.total)) {
|
|
240
|
+
if (r.total === 0) continue;
|
|
241
|
+
const pct = (r.cover * 100).toFixed(1) + '%';
|
|
242
|
+
console.log(` ${r.name.slice(0, w).padEnd(w)} ${String(r.total).padStart(9)} ${pct.padStart(7)} ${String(r.noise).padStart(7)} ${String(r.patterns).padStart(8)} ${(r.npxHooks ? 'YES' : 'no').padStart(5)} ${r.learns ? 'yes' : 'NO'}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// THE FALSIFICATION TEST — stated before the answer is known, so it can actually fail.
|
|
246
|
+
const withMem = readable.filter((r) => r.total >= 50);
|
|
247
|
+
const hookedDead = withMem.filter((r) => r.npxHooks && !r.learns).length;
|
|
248
|
+
const hookedAlive = withMem.filter((r) => r.npxHooks && r.learns).length;
|
|
249
|
+
const cleanAlive = withMem.filter((r) => !r.npxHooks && r.learns).length;
|
|
250
|
+
const cleanDead = withMem.filter((r) => !r.npxHooks && !r.learns).length;
|
|
251
|
+
console.log(`\n HYPOTHESIS: the npx tool-use hooks cause dead memory (unembedded telemetry floods the store).`);
|
|
252
|
+
console.log(` ${'npx hooks + DEAD memory (predicted)'.padEnd(42)} ${hookedDead}`);
|
|
253
|
+
console.log(` ${'npx hooks + LIVE memory (CONTRADICTS)'.padEnd(42)} ${hookedAlive}`);
|
|
254
|
+
console.log(` ${'no hooks + LIVE memory (predicted)'.padEnd(42)} ${cleanAlive}`);
|
|
255
|
+
console.log(` ${'no hooks + DEAD memory (CONTRADICTS)'.padEnd(42)} ${cleanDead}`);
|
|
256
|
+
const contra = hookedAlive + cleanDead;
|
|
257
|
+
console.log(` => ${contra === 0 ? 'hypothesis SURVIVES: no contradicting project' : `hypothesis is INCOMPLETE: ${contra} project(s) contradict it — the npx hooks are NOT the whole story`}`);
|
|
258
|
+
|
|
259
|
+
const unreadable = rows.filter((r) => r.unreadable);
|
|
260
|
+
const schemaless = rows.filter((r) => r.schemaless);
|
|
261
|
+
if (unreadable.length) { console.log(`\n UNREADABLE (NOT "empty" — we could not open these):`); unreadable.forEach((r) => console.log(` ${r.name} — ${r.unreadable}`)); }
|
|
262
|
+
if (schemaless.length) console.log(`\n no memory_entries table (never initialised): ${schemaless.length}`);
|
|
263
|
+
|
|
264
|
+
const dead = readable.filter((r) => !r.learns && r.total > 0);
|
|
265
|
+
console.log(`\n ${readable.filter((r) => r.learns).length} of ${readable.filter((r) => r.total > 0).length} populated stores actually learn. ${dead.length} record and forget.\n`);
|
|
266
|
+
for (const r of dead.slice(0, 10)) console.log(` ${r.name}\n - ${r.findings.join('\n - ')}`);
|
|
267
|
+
|
|
268
|
+
// Non-zero when the fleet is not learning: no scheduled job — and no assistant — can ever again
|
|
269
|
+
// call this "verified" unless the numbers actually agree.
|
|
270
|
+
process.exit(dead.length || unreadable.length ? 1 : 0);
|
|
271
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/model-catalog.mjs — the accessor over data/model-catalog.json (the ADR-0016 verified data).
|
|
3
|
+
// The router-optimizer + console read the user's per-HOUSE frontier from here, so the escalation target
|
|
4
|
+
// and savings baseline are personalized to the person's own stack — Claude shop → Fable 5, ChatGPT/Codex
|
|
5
|
+
// shop → GPT-5.6 Sol, Gemini shop → Gemini 3.1 Pro, Grok shop → Grok 4.5 — never defaulted to one house.
|
|
6
|
+
// Every model returned here is gated by verify-model-catalog.mjs (a wrong/stale one fails the build).
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
14
|
+
|
|
15
|
+
export function loadCatalog(p) {
|
|
16
|
+
const file = p || process.env.RUVNET_MODEL_CATALOG || path.join(ROOT, 'data/model-catalog.json');
|
|
17
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Follow aliasOf (e.g. codex → openai). Returns {name, provider}. */
|
|
21
|
+
function resolve(catalog, name) {
|
|
22
|
+
let p = catalog.providers?.[name];
|
|
23
|
+
if (p?.aliasOf) { name = p.aliasOf; p = catalog.providers?.[name]; }
|
|
24
|
+
return { name, provider: p };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const blend = (e) => (e && Number.isFinite(e.in) && Number.isFinite(e.out) ? +(((e.in + e.out) / 2)).toFixed(2) : null);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Which house is this user? Precedence: explicit config.provider → $RUVNET_PROVIDER → an API key present
|
|
31
|
+
* in env → the catalog default (Anthropic, because this IS a Claude Code plugin — a detected fact, which
|
|
32
|
+
* the caller surfaces with source:'default' so it never reads as an arbitrary house preference).
|
|
33
|
+
* Returns { provider, source: 'config'|'env'|'default' }.
|
|
34
|
+
*/
|
|
35
|
+
export function detectProvider(catalog, { provider, env = process.env } = {}) {
|
|
36
|
+
const P = catalog.providers || {};
|
|
37
|
+
const pick = provider || env.RUVNET_PROVIDER;
|
|
38
|
+
if (pick && pick !== 'auto' && P[pick]) return { provider: pick, source: 'config' };
|
|
39
|
+
// Running inside Claude Code, the dev house is genuinely Anthropic (a fact) — a stray OpenAI key in the
|
|
40
|
+
// env does NOT make you an "OpenAI shop", so don't silently relabel; the person sets that explicitly.
|
|
41
|
+
// Outside Claude Code (CI, standalone), fall to key detection.
|
|
42
|
+
const inClaudeCode = env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT;
|
|
43
|
+
if (!inClaudeCode) {
|
|
44
|
+
for (const name of ['openai', 'google', 'xai', 'anthropic']) {
|
|
45
|
+
const keys = (P[name]?.detect_env || []).filter((k) => !['CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT'].includes(k));
|
|
46
|
+
if (keys.some((k) => env[k])) return { provider: name, source: 'env' };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { provider: catalog.default_provider || 'anthropic', source: 'default' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The frontier (flagship) for a house: {model, in, out, costPerMTok, rank, released, provider, label}. */
|
|
53
|
+
export function frontierFor(catalog, name) {
|
|
54
|
+
const { name: r, provider: p } = resolve(catalog, name);
|
|
55
|
+
if (!p?.frontier) return null;
|
|
56
|
+
return { ...p.frontier, costPerMTok: blend(p.frontier), provider: r, label: p.label };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The full house ladder (cheap/mid/frontier — any may be null). */
|
|
60
|
+
export function ladderFor(catalog, name) {
|
|
61
|
+
const { name: r, provider: p } = resolve(catalog, name);
|
|
62
|
+
const t = (e) => (e ? { ...e, costPerMTok: blend(e) } : null);
|
|
63
|
+
return { provider: r, label: p?.label || r, cheap: t(p?.cheap), mid: t(p?.mid), frontier: t(p?.frontier) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function providerLabel(catalog, name) { return resolve(catalog, name).provider?.label || name; }
|
|
67
|
+
|
|
68
|
+
/** The set of houses a user can pick, for the console selector: [{ id, label }]. */
|
|
69
|
+
export function providerChoices(catalog) {
|
|
70
|
+
return Object.entries(catalog.providers || {}).map(([id, p]) => ({ id, label: p.label || id }));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
74
|
+
const c = loadCatalog();
|
|
75
|
+
const det = detectProvider(c, {});
|
|
76
|
+
console.log(`detected house: ${det.provider} (${det.source})`);
|
|
77
|
+
console.log('frontier:', JSON.stringify(frontierFor(c, det.provider)));
|
|
78
|
+
console.log('choices:', providerChoices(c).map((x) => x.id).join(', '));
|
|
79
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// nightly-controller.mjs — a thin adapter around the installer's one scheduler implementation.
|
|
2
|
+
//
|
|
3
|
+
// It does not write a plist, call launchctl, or invent platform behavior. Both the installer and the
|
|
4
|
+
// console reach the same `bin/install.mjs --enable-nightly/--disable-nightly` door; this adapter only
|
|
5
|
+
// supplies structured status and captures its exit result for the console.
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
14
|
+
const INSTALLER = path.join(ROOT, 'bin', 'install.mjs');
|
|
15
|
+
const LABEL = 'com.ruvnet.brain-update';
|
|
16
|
+
|
|
17
|
+
export function nightlyArtifact({ env = process.env, platform = process.platform } = {}) {
|
|
18
|
+
const home = env.HOME || os.homedir();
|
|
19
|
+
return {
|
|
20
|
+
supported: platform === 'darwin',
|
|
21
|
+
platform,
|
|
22
|
+
path: path.join(home, 'Library', 'LaunchAgents', `${LABEL}.plist`),
|
|
23
|
+
label: LABEL,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function nightlyStatus(options = {}) {
|
|
28
|
+
const artifact = nightlyArtifact(options);
|
|
29
|
+
if (!artifact.supported) {
|
|
30
|
+
return { state: 'unsupported', evidence: `No reversible scheduler adapter is implemented for ${artifact.platform}.`, artifact };
|
|
31
|
+
}
|
|
32
|
+
const present = fs.existsSync(artifact.path);
|
|
33
|
+
return {
|
|
34
|
+
state: present ? 'on' : 'off',
|
|
35
|
+
evidence: present ? `LaunchAgent plist exists at ${artifact.path}` : `No LaunchAgent plist at ${artifact.path}`,
|
|
36
|
+
artifact,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function applyNightlyChoice(enabled, options = {}) {
|
|
41
|
+
if (typeof enabled !== 'boolean') return { ok: false, log: 'nightly must be true or false' };
|
|
42
|
+
const env = options.env || process.env;
|
|
43
|
+
const before = nightlyStatus({ ...options, env });
|
|
44
|
+
if (!before.artifact.supported) return { ok: false, state: before, log: before.evidence };
|
|
45
|
+
const run = spawnSync(process.execPath, [
|
|
46
|
+
options.installer || INSTALLER,
|
|
47
|
+
enabled ? '--enable-nightly' : '--disable-nightly',
|
|
48
|
+
], {
|
|
49
|
+
env: { ...env, RUVNET_BRAIN_IMPORT_ONLY: '0' },
|
|
50
|
+
cwd: options.cwd || ROOT,
|
|
51
|
+
encoding: 'utf8',
|
|
52
|
+
shell: false,
|
|
53
|
+
timeout: options.timeout || 30_000,
|
|
54
|
+
});
|
|
55
|
+
const after = nightlyStatus({ ...options, env });
|
|
56
|
+
const desired = enabled ? 'on' : 'off';
|
|
57
|
+
const ok = !run.error && run.status === 0 && after.state === desired;
|
|
58
|
+
return {
|
|
59
|
+
ok,
|
|
60
|
+
before,
|
|
61
|
+
after,
|
|
62
|
+
log: ok
|
|
63
|
+
? `Nightly refresh is ${desired}; verified from ${after.artifact.path}.`
|
|
64
|
+
: `Nightly refresh did not reach ${desired}: ${run.error?.message || run.stderr?.trim() || run.stdout?.trim() || `exit ${run.status}`}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# nightly-gists.sh — keep the brain's view of rUv's thinking current, cheaply.
|
|
3
|
+
#
|
|
4
|
+
# Run by the launchd agent com.ruvnet.brain-gists (21:47 local). Does NOT publish, deploy, push, or
|
|
5
|
+
# touch anything outward-facing. Reads GitHub, writes to kb/ and docs/.
|
|
6
|
+
#
|
|
7
|
+
# COST DISCIPLINE (the whole point):
|
|
8
|
+
# 1. index refresh — ~5 API calls, no per-gist fetch, no model. Always.
|
|
9
|
+
# 2. content ingest — incremental; only re-fetches gists whose updated_at moved.
|
|
10
|
+
# 3. re-embed — the expensive step (~18 min, 8 shards). SKIPPED ENTIRELY when step 2
|
|
11
|
+
# reports "nothing to do". A quiet night costs seconds, not minutes.
|
|
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.
|
|
16
|
+
|
|
17
|
+
set -eu
|
|
18
|
+
|
|
19
|
+
cd "$(dirname "$0")/.."
|
|
20
|
+
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
|
|
21
|
+
export KB_MODEL_CACHE="${KB_MODEL_CACHE:-/Users/stuartkerr/Code/PowerPlatePulse/scripts/models-cache}"
|
|
22
|
+
|
|
23
|
+
mkdir -p logs
|
|
24
|
+
LOG="logs/gists-nightly.log"
|
|
25
|
+
ts() { date '+%Y-%m-%dT%H:%M:%S%z'; }
|
|
26
|
+
log() { printf '[%s] %s\n' "$(ts)" "$1" >>"$LOG"; }
|
|
27
|
+
|
|
28
|
+
log "start"
|
|
29
|
+
|
|
30
|
+
command -v gh >/dev/null 2>&1 || { log "FATAL: gh not on PATH"; exit 1; }
|
|
31
|
+
gh auth status >/dev/null 2>&1 || { log "FATAL: gh not authenticated"; exit 1; }
|
|
32
|
+
|
|
33
|
+
# 1 — the tracked index. Cheap enough to do unconditionally.
|
|
34
|
+
node scripts/ingest-gists.mjs --index-only >>"$LOG" 2>&1
|
|
35
|
+
log "index refreshed"
|
|
36
|
+
|
|
37
|
+
# 2 — incremental content ingest.
|
|
38
|
+
if ! OUT=$(node scripts/ingest-gists.mjs 2>&1); then
|
|
39
|
+
printf '%s\n' "$OUT" >>"$LOG"
|
|
40
|
+
log "FATAL: ingest failed"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
printf '%s\n' "$OUT" >>"$LOG"
|
|
44
|
+
|
|
45
|
+
if printf '%s' "$OUT" | grep -q 'nothing to do'; then
|
|
46
|
+
log "no new gists — skipping embed (cost: 0)"
|
|
47
|
+
exit 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# 3 — re-embed, only because the corpus actually changed. 8 shards, then one assemble.
|
|
51
|
+
# DERIVED, not asserted (F6, 2026-07-18): a bare `wait` with no operands ALWAYS returns 0 under
|
|
52
|
+
# POSIX, so a failed shard was structurally invisible — the script would proceed to ingest and log
|
|
53
|
+
# "done — rebuilt" over a half-embedded corpus. Now every shard PID is waited on individually and a
|
|
54
|
+
# single failure aborts BEFORE ingest, loudly. "done" is only printed over a fully-embedded corpus.
|
|
55
|
+
log "corpus changed — re-embedding"
|
|
56
|
+
i=0
|
|
57
|
+
pids=""
|
|
58
|
+
while [ "$i" -lt 8 ]; do
|
|
59
|
+
node kb/forge-big.mjs embed --dir kb --name ruv-gists --shard "$i" --of 8 >>"$LOG" 2>&1 &
|
|
60
|
+
pids="$pids $!"
|
|
61
|
+
i=$((i + 1))
|
|
62
|
+
done
|
|
63
|
+
FAILED_SHARDS=0
|
|
64
|
+
for p in $pids; do
|
|
65
|
+
wait "$p" || FAILED_SHARDS=$((FAILED_SHARDS + 1))
|
|
66
|
+
done
|
|
67
|
+
if [ "$FAILED_SHARDS" -gt 0 ]; then
|
|
68
|
+
log "EMBED FAILED — $FAILED_SHARDS of 8 shards exited nonzero; refusing to ingest a half-embedded corpus"
|
|
69
|
+
exit 1
|
|
70
|
+
fi
|
|
71
|
+
node kb/forge-big.mjs ingest --dir kb --name ruv-gists >>"$LOG" 2>&1 || { log "INGEST FAILED — store NOT rebuilt"; exit 1; }
|
|
72
|
+
log "done — ruv-gists store rebuilt"
|