ruvnet-brain 4.0.1 → 4.0.4
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 +303 -24
- 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/commands/whats-new.md +6 -6
- package/plugin/docs/RELEASE-NOTES-4.0.md +88 -0
- package/plugin/hooks/hooks.json +1 -2
- package/plugin/mcp/managed-cli-interface.mjs +47 -4
- package/plugin/mcp/server.mjs +90 -32
- 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 +34 -29
- 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 +503 -0
- package/plugin/scripts/session-start.sh +3 -858
- package/plugin/scripts/whats-new.mjs +42 -0
- package/plugin/skills/brain-console/SKILL.md +4 -2
- package/plugin/skills/release-proof/SKILL.md +98 -0
- package/plugin/skills/release-proof/agents/openai.yaml +4 -0
- package/plugin/skills/release-proof/references/receipt-contract.md +44 -0
- package/plugin/skills/release-proof/scripts/release-proof.mjs +286 -0
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +5 -1
- package/plugin/skills/ruvnet-brain/SKILL.md +22 -7
- package/plugin/skills/rvbc/SKILL.md +9 -6
- package/plugin/skills/whats-new/SKILL.md +4 -4
- 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 +531 -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/fix-workstream.mjs +291 -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 +237 -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 +558 -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 +326 -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 +172 -0
- package/scripts/notify.sh +12 -0
- package/scripts/npx-witness.sh +56 -0
- package/scripts/onboarding-console.mjs +2922 -0
- package/scripts/private-fence.mjs +69 -0
- package/scripts/proactivity-metrics.mjs +118 -0
- package/scripts/proof-questions.json +56 -0
- package/scripts/protected-release-invocation.mjs +76 -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/publication-receipt.mjs +307 -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-authority.mjs +93 -0
- package/scripts/release-proof.mjs +9 -0
- package/scripts/release-vector.mjs +281 -0
- package/scripts/release.mjs +439 -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 +296 -0
- package/scripts/selfcheck.mjs +7 -1
- package/scripts/sign-bundle.mjs +69 -0
- package/scripts/signal-watch.mjs +171 -0
- package/scripts/stabilization-receipt.mjs +108 -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 +867 -0
- package/plugin/scripts/finalize-token-meter.mjs +0 -25
package/plugin/mcp/server.mjs
CHANGED
|
@@ -41,14 +41,15 @@ const CHILD_MCP = process.env.RUVNET_BRAIN_CHILD_MCP || path.join(KB, 'forge-mcp
|
|
|
41
41
|
const ACTIVE = path.join(BRAIN_HOME, 'active.json');
|
|
42
42
|
const LEASES = path.join(BRAIN_HOME, 'leases');
|
|
43
43
|
const LEASE = path.join(LEASES, `mcp-${process.pid}.json`);
|
|
44
|
+
const READINESS = path.join(BRAIN_HOME, 'mcp-readiness.json');
|
|
44
45
|
|
|
45
46
|
const PROTOCOL_VERSION = '2024-11-05';
|
|
46
47
|
const SERVER_INFO = { name: 'ruvnet-brain', version: '2.0.0' };
|
|
47
|
-
//
|
|
48
|
-
//
|
|
48
|
+
// Stable declarations belong to this protocol shell. Capability discovery must never wait for a
|
|
49
|
+
// model or vector store; the first operation that needs the worker joins its readiness promise.
|
|
49
50
|
const SEARCH_TOOL = {
|
|
50
51
|
name: 'search_ruvnet',
|
|
51
|
-
description: 'Source-grounded knowledge base for the RuvNet ecosystem.
|
|
52
|
+
description: 'Source-grounded knowledge base for the RuvNet ecosystem. The first call may wait while the local search worker becomes ready.',
|
|
52
53
|
inputSchema: {
|
|
53
54
|
type: 'object',
|
|
54
55
|
properties: {
|
|
@@ -60,22 +61,45 @@ const SEARCH_TOOL = {
|
|
|
60
61
|
};
|
|
61
62
|
const FALLBACK_TOOLS = [SEARCH_TOOL, ...MANAGED_CLI_TOOLS];
|
|
62
63
|
|
|
63
|
-
function withLocalTools(tools) {
|
|
64
|
-
const merged = new Map();
|
|
65
|
-
for (const tool of Array.isArray(tools) ? tools : []) {
|
|
66
|
-
if (tool?.name) merged.set(tool.name, tool);
|
|
67
|
-
}
|
|
68
|
-
for (const tool of FALLBACK_TOOLS) {
|
|
69
|
-
if (!merged.has(tool.name)) merged.set(tool.name, tool);
|
|
70
|
-
}
|
|
71
|
-
return [...merged.values()];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
64
|
const out = (obj) => process.stdout.write(JSON.stringify(obj) + '\n');
|
|
75
65
|
const clientOk = (id, result) => out({ jsonrpc: '2.0', id, result });
|
|
76
66
|
const clientErr = (id, code, message) => out({ jsonrpc: '2.0', id, error: { code, message } });
|
|
77
67
|
const readJSON = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } };
|
|
78
68
|
|
|
69
|
+
function writeReadiness(value) {
|
|
70
|
+
try {
|
|
71
|
+
fs.mkdirSync(BRAIN_HOME, { recursive: true });
|
|
72
|
+
const tmp = `${READINESS}.${process.pid}.${Date.now()}.tmp`;
|
|
73
|
+
fs.writeFileSync(tmp, `${JSON.stringify({ ...value, pid: process.pid, at: new Date().toISOString() })}\n`, { mode: 0o600 });
|
|
74
|
+
fs.renameSync(tmp, READINESS);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error(`[ruvnet-brain] could not persist MCP readiness: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function recordStartupFailure({ phase, startedAt, generation, error }) {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
const fields = {
|
|
83
|
+
state: 'degraded', phase, generation, elapsedMs: now - startedAt,
|
|
84
|
+
retryable: true, retryState: 'next-search-retries',
|
|
85
|
+
error: String(error?.message || error || 'unknown startup failure').slice(0, 500),
|
|
86
|
+
};
|
|
87
|
+
writeReadiness(fields);
|
|
88
|
+
const healthPath = path.join(BRAIN_HOME, 'health.json');
|
|
89
|
+
try {
|
|
90
|
+
fs.mkdirSync(BRAIN_HOME, { recursive: true });
|
|
91
|
+
const tmp = `${healthPath}.${process.pid}.${now}.tmp`;
|
|
92
|
+
fs.writeFileSync(tmp, `${JSON.stringify({
|
|
93
|
+
status: 'down', source: 'mcp-parent-startup', phase,
|
|
94
|
+
error: fields.error, elapsedMs: fields.elapsedMs, generation,
|
|
95
|
+
retryState: fields.retryState, ts: new Date(now).toISOString(),
|
|
96
|
+
})}\n`, { mode: 0o600 });
|
|
97
|
+
fs.renameSync(tmp, healthPath);
|
|
98
|
+
} catch (writeError) {
|
|
99
|
+
console.error(`[ruvnet-brain] could not persist startup health: ${writeError.message}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
79
103
|
function currentGeneration() {
|
|
80
104
|
const a = readJSON(ACTIVE);
|
|
81
105
|
let brainMtime = 0;
|
|
@@ -124,6 +148,7 @@ function killChild(reason) {
|
|
|
124
148
|
clearChildIdleTimer();
|
|
125
149
|
if (!child) return childRetirement;
|
|
126
150
|
const c = child; child = null;
|
|
151
|
+
c.intentionalStop = true;
|
|
127
152
|
for (const [, p] of c.pending) p.reject(new Error(`brain worker ${reason}`));
|
|
128
153
|
c.pending.clear();
|
|
129
154
|
childRetirement = new Promise((resolve) => {
|
|
@@ -147,16 +172,22 @@ function killChild(reason) {
|
|
|
147
172
|
async function ensureChild() {
|
|
148
173
|
const gen = currentGeneration();
|
|
149
174
|
if (child && child.generation !== gen && pendingCount === 0) await killChild('superseded by a newer generation');
|
|
175
|
+
// A concurrent initialize/tools-list/tools-call must join the same readiness promise. Returning
|
|
176
|
+
// `child` while its private initialize or model warmup is still running advertises a half-ready
|
|
177
|
+
// worker and charges cold-start to the first user call.
|
|
178
|
+
if (childStartup) return childStartup;
|
|
150
179
|
if (child) return child;
|
|
151
180
|
await childRetirement;
|
|
152
181
|
if (!fs.existsSync(CHILD_MCP)) return null;
|
|
153
182
|
|
|
154
183
|
if (!childStartup) {
|
|
155
184
|
childStartup = (async () => {
|
|
185
|
+
const startedAt = Date.now();
|
|
186
|
+
let phase = 'initialize';
|
|
156
187
|
const env = { ...process.env, KB_DIR: KB };
|
|
157
188
|
if (!env.KB_MODEL_CACHE) env.KB_MODEL_CACHE = path.join(BRAIN_HOME, 'models');
|
|
158
189
|
const proc = spawn(process.execPath, [CHILD_MCP], { stdio: ['pipe', 'pipe', 'inherit'], env });
|
|
159
|
-
const c = { proc, generation: currentGeneration(), nextId: 1, pending: new Map() };
|
|
190
|
+
const c = { proc, generation: currentGeneration(), nextId: 1, pending: new Map(), intentionalStop: false };
|
|
160
191
|
const rl = readline.createInterface({ input: proc.stdout });
|
|
161
192
|
rl.on('line', (line) => {
|
|
162
193
|
let msg; try { msg = JSON.parse(line); } catch { return; }
|
|
@@ -167,6 +198,13 @@ async function ensureChild() {
|
|
|
167
198
|
if (child === c) child = null;
|
|
168
199
|
for (const [, p] of c.pending) p.reject(new Error('brain worker exited'));
|
|
169
200
|
c.pending.clear();
|
|
201
|
+
if (!c.intentionalStop) {
|
|
202
|
+
writeReadiness({
|
|
203
|
+
state: 'degraded', phase: 'worker-exit', generation: c.generation,
|
|
204
|
+
elapsedMs: Date.now() - startedAt, retryable: true,
|
|
205
|
+
retryState: 'next-search-retries', error: 'brain worker exited unexpectedly',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
170
208
|
});
|
|
171
209
|
proc.on('error', () => { if (child === c) child = null; });
|
|
172
210
|
child = c;
|
|
@@ -176,11 +214,29 @@ async function ensureChild() {
|
|
|
176
214
|
protocolVersion: PROTOCOL_VERSION,
|
|
177
215
|
capabilities: {},
|
|
178
216
|
clientInfo: { name: 'ruvnet-brain-shell', version: SERVER_INFO.version },
|
|
179
|
-
}, CHILD_INIT_TIMEOUT_MS);
|
|
217
|
+
}, CHILD_INIT_TIMEOUT_MS, { reportTimeout: false });
|
|
218
|
+
phase = 'warmup';
|
|
219
|
+
const warmed = await childRequest(c, 'brain/warmup', {}, CHILD_INIT_TIMEOUT_MS, { reportTimeout: false });
|
|
220
|
+
// Rolling-upgrade compatibility: 4.0.1 workers predate the private warmup extension. The
|
|
221
|
+
// stable shell is installed before the 700MB KB swap completes, so rejecting -32601 here
|
|
222
|
+
// makes every query fail during the exact in-place upgrade window the shell exists to hide.
|
|
223
|
+
// An older initialized worker is usable (its first query may be cold); any other warmup
|
|
224
|
+
// failure still fails closed.
|
|
225
|
+
const warmupUnsupported = warmed.error?.code === -32601
|
|
226
|
+
|| /method not found/i.test(String(warmed.error?.message || ''));
|
|
227
|
+
if (!warmupUnsupported && (warmed.error || warmed.result?.ready !== true)) {
|
|
228
|
+
throw new Error(warmed.error?.message || 'brain worker did not confirm warm readiness');
|
|
229
|
+
}
|
|
180
230
|
} catch (e) {
|
|
181
231
|
await killChild('failed initialize');
|
|
232
|
+
recordStartupFailure({ phase, startedAt, generation: c.generation, error: e });
|
|
182
233
|
throw new Error(`brain worker failed to initialize: ${e.message}`);
|
|
183
234
|
}
|
|
235
|
+
writeReadiness({
|
|
236
|
+
state: 'ready', phase, generation: c.generation,
|
|
237
|
+
workerPid: c.proc.pid, elapsedMs: Date.now() - startedAt,
|
|
238
|
+
retryable: false, retryState: 'none',
|
|
239
|
+
});
|
|
184
240
|
armChildIdleTimer(c);
|
|
185
241
|
return c;
|
|
186
242
|
})();
|
|
@@ -221,13 +277,13 @@ async function onChildTimeout(method, timeoutMs) {
|
|
|
221
277
|
// Steady-state latency remains a separate release gate; this budget prevents a permanent cold loop.
|
|
222
278
|
const CALL_TIMEOUT_MS = Number(process.env.RUVNET_BRAIN_CALL_TIMEOUT_MS) || 240_000;
|
|
223
279
|
|
|
224
|
-
function childRequest(c, method, params, timeoutMs = CALL_TIMEOUT_MS) {
|
|
280
|
+
function childRequest(c, method, params, timeoutMs = CALL_TIMEOUT_MS, { reportTimeout = true } = {}) {
|
|
225
281
|
return new Promise((resolve, reject) => {
|
|
226
282
|
const id = c.nextId++;
|
|
227
283
|
const timer = setTimeout(() => {
|
|
228
284
|
c.pending.delete(id);
|
|
229
285
|
reject(new Error(`brain worker timeout on ${method}`));
|
|
230
|
-
void onChildTimeout(method, timeoutMs); //
|
|
286
|
+
if (reportTimeout) void onChildTimeout(method, timeoutMs); // caller never waits on the alarm
|
|
231
287
|
}, timeoutMs);
|
|
232
288
|
c.pending.set(id, { resolve: (m) => { clearTimeout(timer); resolve(m); }, reject: (e) => { clearTimeout(timer); reject(e); } });
|
|
233
289
|
try { c.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n'); }
|
|
@@ -241,23 +297,16 @@ async function handleClient(msg) {
|
|
|
241
297
|
if (id === undefined || id === null) return; // notifications need no answer
|
|
242
298
|
switch (method) {
|
|
243
299
|
case 'initialize':
|
|
300
|
+
// MCP initialize itself stays protocol-fast; normal clients request tools/list immediately
|
|
301
|
+
// afterwards, which joins this readiness promise and does not receive the tool declaration
|
|
302
|
+
// until the worker's query embedder and cross-encoder are warm.
|
|
303
|
+
void ensureChild().catch((e) => {
|
|
304
|
+
console.error(`[ruvnet-brain] worker warmup failed during initialize: ${e.message}`);
|
|
305
|
+
});
|
|
244
306
|
return clientOk(id, { protocolVersion: PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: SERVER_INFO });
|
|
245
307
|
case 'ping':
|
|
246
308
|
return clientOk(id, {});
|
|
247
309
|
case 'tools/list': {
|
|
248
|
-
try {
|
|
249
|
-
const c = await ensureChild();
|
|
250
|
-
if (c) {
|
|
251
|
-
try {
|
|
252
|
-
const r = await childRequest(c, 'tools/list', {}, 15_000);
|
|
253
|
-
if (r.result?.tools?.length) {
|
|
254
|
-
armChildIdleTimer(c);
|
|
255
|
-
return clientOk(id, { ...r.result, tools: withLocalTools(r.result.tools) });
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
catch { /* fall through to the static declaration */ }
|
|
259
|
-
}
|
|
260
|
-
} catch { /* fall through to the static declaration during a transient startup outage */ }
|
|
261
310
|
return clientOk(id, { tools: FALLBACK_TOOLS });
|
|
262
311
|
}
|
|
263
312
|
case 'tools/call': {
|
|
@@ -300,4 +349,13 @@ clientRl.on('line', (line) => {
|
|
|
300
349
|
let msg; try { msg = JSON.parse(line); } catch { return; } // malformed line: ignore, never crash
|
|
301
350
|
handleClient(msg).catch((e) => { if (msg.id !== undefined && msg.id !== null) clientErr(msg.id, -32603, e.message); });
|
|
302
351
|
});
|
|
303
|
-
|
|
352
|
+
let shuttingDown = false;
|
|
353
|
+
async function shutdown(reason) {
|
|
354
|
+
if (shuttingDown) return;
|
|
355
|
+
shuttingDown = true;
|
|
356
|
+
await killChild(reason);
|
|
357
|
+
process.exit(0);
|
|
358
|
+
}
|
|
359
|
+
clientRl.on('close', () => { void shutdown('client disconnected'); });
|
|
360
|
+
process.once('SIGTERM', () => { void shutdown('parent received SIGTERM'); });
|
|
361
|
+
process.once('SIGINT', () => { void shutdown('parent received SIGINT'); });
|
|
@@ -107,11 +107,25 @@ if (!SUPERVISOR) {
|
|
|
107
107
|
? path.join(process.env.SystemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
|
|
108
108
|
: 'powershell.exe';
|
|
109
109
|
const quotePs = (value) => `'${String(value).replaceAll("'", "''")}'`;
|
|
110
|
+
// Start-Process has its own standard-stream boundary. On GitHub's packed PowerShell install
|
|
111
|
+
// path the SessionStart body finished but the external checker never received `close` before
|
|
112
|
+
// its 5s watchdog — consistent with a descendant retaining a capture handle, though that job
|
|
113
|
+
// did not instrument exact handle ownership. Redirect both streams to distinct files at the
|
|
114
|
+
// native boundary so the supervisor cannot inherit the hook's capture streams; redirecting
|
|
115
|
+
// only this short Node wrapper's stdio does not establish that contract for its grandchild.
|
|
116
|
+
const supervisorOut = logPath && logPath !== '-'
|
|
117
|
+
? `${logPath}.supervisor.stdout`
|
|
118
|
+
: path.join(os.tmpdir(), `ruvnet-brain-detach-supervisor-${process.pid}.stdout`);
|
|
119
|
+
const supervisorErr = logPath && logPath !== '-'
|
|
120
|
+
? `${logPath}.supervisor.stderr`
|
|
121
|
+
: path.join(os.tmpdir(), `ruvnet-brain-detach-supervisor-${process.pid}.stderr`);
|
|
110
122
|
const launch = [
|
|
111
123
|
'Start-Process',
|
|
112
124
|
'-FilePath', quotePs(process.execPath),
|
|
113
125
|
'-ArgumentList', `@(${quotePs(SELF)},${quotePs('--payload-env')})`,
|
|
114
126
|
'-WindowStyle', 'Hidden',
|
|
127
|
+
'-RedirectStandardOutput', quotePs(supervisorOut),
|
|
128
|
+
'-RedirectStandardError', quotePs(supervisorErr),
|
|
115
129
|
].join(' ');
|
|
116
130
|
child = spawn(powershell, ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', launch], {
|
|
117
131
|
detached: true,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// One detached first-session lifecycle worker: seed the Stable Spine, then perform the update
|
|
3
|
+
// heartbeat the seed used to race. Keeping both operations in one worker preserves ADR-054's
|
|
4
|
+
// "Brain OFF still receives fixes" contract without making SessionStart launch two detachers.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
const [updateApply, hostUpdate, versionLog] = process.argv.slice(2);
|
|
10
|
+
if (![updateApply, hostUpdate, versionLog].every((value) => typeof value === 'string' && value)) {
|
|
11
|
+
process.stderr.write('usage: first-session-worker.mjs <update-apply.mjs> <host-update.mjs> <version-log>\n');
|
|
12
|
+
process.exit(2);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const seed = spawnSync(process.execPath, [updateApply, '--seed'], {
|
|
16
|
+
env: process.env,
|
|
17
|
+
stdio: 'inherit',
|
|
18
|
+
});
|
|
19
|
+
if (seed.error || seed.status !== 0) {
|
|
20
|
+
process.stderr.write(`[ruvnet-brain] first-session seed failed: ${seed.error?.message || `exit ${seed.status}`}\n`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const check = spawnSync(process.execPath, [hostUpdate, '--check'], {
|
|
25
|
+
env: process.env,
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
});
|
|
28
|
+
const version = String(check.stdout || '').trim();
|
|
29
|
+
if (!check.error && check.status === 0 && /^[0-9]+\.[0-9]+\.[0-9]+(?:-[A-Za-z0-9.-]+)?$/.test(version)) {
|
|
30
|
+
fs.mkdirSync(path.dirname(versionLog), { recursive: true });
|
|
31
|
+
const temporary = `${versionLog}.tmp-${process.pid}`;
|
|
32
|
+
fs.writeFileSync(temporary, `${version}\n`);
|
|
33
|
+
fs.renameSync(temporary, versionLog);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Network failure must not undo a successful seed. The 15-minute heartbeat stamp lets the normal
|
|
37
|
+
// SessionStart path retry later, while the current runtime remains valid and usable.
|
|
38
|
+
process.exit(0);
|
|
@@ -121,7 +121,7 @@ check_memory_db() {
|
|
|
121
121
|
for _db in .swarm/agentdb-memory.db .swarm/memory.db .claude/memory.db; do
|
|
122
122
|
check_memory_db "$_db" && break
|
|
123
123
|
done
|
|
124
|
-
if [ "$MEM_STATE"
|
|
124
|
+
if [ "$MEM_STATE" != "on" ]; then
|
|
125
125
|
_common_git=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)
|
|
126
126
|
case "$_common_git" in
|
|
127
127
|
*/.git)
|
|
@@ -136,6 +136,15 @@ if [ "$MEM_STATE" = "off" ]; then
|
|
|
136
136
|
;;
|
|
137
137
|
esac
|
|
138
138
|
fi
|
|
139
|
+
if [ "$MEM_STATE" != "on" ] && [ -n "${HOME:-}" ] && [ "$PWD" != "$HOME" ]; then
|
|
140
|
+
for _db in \
|
|
141
|
+
"$HOME/.swarm/agentdb-memory.db" \
|
|
142
|
+
"$HOME/.swarm/memory.db" \
|
|
143
|
+
"$HOME/.claude/memory.db"
|
|
144
|
+
do
|
|
145
|
+
check_memory_db "$_db" && break
|
|
146
|
+
done
|
|
147
|
+
fi
|
|
139
148
|
unset _db
|
|
140
149
|
unset _common_git _primary_root
|
|
141
150
|
# RUNNING version (this session's loaded plugin) vs STAGED version (marketplace copy on disk).
|
|
@@ -162,11 +171,10 @@ if [ "$RUFLO_STATE" = "yes" ] && [ "$MEM_STATE" = "off" ]; then
|
|
|
162
171
|
This project runs the Ruflo stack but AgentDB persistent project memory is NOT set up (.swarm/memory.db does not exist) — decisions made here are being lost between sessions. rUv's default is memory ON. If you have not already offered this session, offer ONCE, plainly and warmly: "One thing I noticed: this project doesn't have persistent memory turned on — AgentDB would let me carry decisions and context across sessions instead of starting cold each time. Want me to turn it on and wire it up?" On a yes, set it up with the ruflo tools you have (a first memory_store write creates the store) and confirm with the real file path. If they decline, respect it for the rest of the session — the status line keeps them informed without nagging.
|
|
163
172
|
EOF
|
|
164
173
|
fi
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
fi
|
|
174
|
+
# A stale mtime is not a user-facing event. SessionEnd/PreCompact own capture;
|
|
175
|
+
# their persistence receipt must fail closed at the write boundary. Emitting a
|
|
176
|
+
# UserPromptSubmit instruction here interrupted unrelated work and still could
|
|
177
|
+
# not prove that the store command wrote the canonical database.
|
|
170
178
|
|
|
171
179
|
# ── Self-learning flywheel (ruflo ≥3.24, ADR-176) — OFFER it, never switch it on for them ────────
|
|
172
180
|
# Opt-in is a single env var; `harnessLoopOptedIn()` in @claude-flow/cli reads process.env directly,
|
|
@@ -375,6 +383,7 @@ This task touches the RuvNet / rUv stack. Your training priors here are STALE an
|
|
|
375
383
|
- You MUST call the `search_ruvnet` MCP tool and ground your answer in the cited source paths it returns BEFORE stating what any RuvNet tool can/cannot do or which one to use. Asserting a RuvNet capability from memory, without a cited source path, is an error — do not do it.
|
|
376
384
|
- If a needed RuvNet repo isn't covered yet, ingest it first (`node scripts/ingest-repo.mjs --name <repo>`) rather than guessing.
|
|
377
385
|
- SIGNAL that the brain is working so the user has confidence it's in charge: when you ground an answer, say so briefly (e.g. "Grounding in rUv's source…" → cite the path). Silent grounding leaves the user unsure it's even on.
|
|
386
|
+
- MEMORY DIAGNOSIS: never infer a broken write from DB/WAL mtime, semantic-search misses, daemon startup, or the CLI success line. Prove it against ONE canonical absolute path: unique-key `ruflo memory store --path <project>/.swarm/memory.db` → exact-key `ruflo memory retrieve --path ...` → exact SQL row. Only then call it miswired.
|
|
378
387
|
EOF
|
|
379
388
|
fi
|
|
380
389
|
|
|
@@ -398,6 +407,7 @@ This is a build / change request — run THE PLAYBOOK (the standing build playbo
|
|
|
398
407
|
- Read the actual files in THEIR repo this touches — what pattern do they already use? what would duplicate?
|
|
399
408
|
- Call `search_ruvnet` for what the feature technically DOES — never trust memory about what the corpus has.
|
|
400
409
|
- Check project memory (ruflo memory search / AgentDB) for prior decisions on this area.
|
|
410
|
+
- If memory looks broken, use the exact-path store → exact-key retrieve → exact SQL proof; never diagnose from semantic search, mtime, daemon startup, or a success message.
|
|
401
411
|
⛔ NO SILENT SUBSTITUTION: use the real RuvNet tool, or say out loud that you're hand-rolling and why.
|
|
402
412
|
Senior partner: one plan, momentum, end with real work.
|
|
403
413
|
EOF
|
|
@@ -75,23 +75,23 @@ catch (e) { BRAIN_OFF = !(e && (e.code === 'ENOENT' || e.code === 'ENOTDIR')); }
|
|
|
75
75
|
// stops a subagent fan-out inheriting an expensive model; design-wall stops an
|
|
76
76
|
// ungraded surface shipping; protect-state guards the user's own consent record.
|
|
77
77
|
// None becomes acceptable because retrieval is off.
|
|
78
|
-
// 'partial' — the hook splits INTERNALLY. session-start still runs the auto-updater heartbeat, the
|
|
78
|
+
// 'partial' — the hook splits INTERNALLY. session-start-core still runs the auto-updater heartbeat, the
|
|
79
79
|
// GONG health alarm and the SLA banner (an off machine must still receive fixes,
|
|
80
80
|
// otherwise the fix for an off-state bug can never arrive) while suppressing every
|
|
81
81
|
// advertising byte. The snapshot is forwarded as RUVNET_BRAIN_OFF so the body reads
|
|
82
82
|
// ONE resolved answer instead of racing the filesystem again mid-run.
|
|
83
83
|
const TABLE = {
|
|
84
|
-
'session-start': { file: 'session-start.
|
|
85
|
-
'ground-ruvnet': { file: 'ground-ruvnet.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence' },
|
|
84
|
+
'session-start': { file: 'session-start-core.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'partial' },
|
|
85
|
+
'ground-ruvnet': { file: 'ground-ruvnet.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence', stdinBytes: 32768 },
|
|
86
86
|
'hijack-ruvnet': { file: 'hijack-ruvnet.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence' },
|
|
87
|
-
'route-dispatch': { file: 'route-dispatch.sh', interpreter: 'bash', mode: 'blocking', offBehavior: 'run' },
|
|
88
|
-
'ground-before-write': { file: 'ground-before-write.sh', interpreter: 'bash', mode: 'blocking', offBehavior: 'run' },
|
|
87
|
+
'route-dispatch': { file: 'route-dispatch.sh', interpreter: 'bash', mode: 'blocking', offBehavior: 'run', stdinBytes: 65536 },
|
|
88
|
+
'ground-before-write': { file: 'ground-before-write.sh', interpreter: 'bash', mode: 'blocking', offBehavior: 'run', stdinBytes: 65536 },
|
|
89
89
|
'grounding-stamp': { file: 'grounding-stamp.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence' },
|
|
90
90
|
'verify-interface': { file: 'verify-interface.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence' },
|
|
91
|
-
'design-wall': { file: 'design-wall.sh', interpreter: 'bash', mode: 'blocking', offBehavior: 'run' },
|
|
91
|
+
'design-wall': { file: 'design-wall.sh', interpreter: 'bash', mode: 'blocking', offBehavior: 'run', stdinBytes: 65536 },
|
|
92
92
|
// The consent guard (ADR-054 §3): it protects the OFF state itself, so it is the one hook that
|
|
93
93
|
// matters MORE while the brain is off. 'run', permanently.
|
|
94
|
-
'protect-state': { file: 'protect-brain-state.sh', interpreter: 'bash', mode: 'blocking', offBehavior: 'run' },
|
|
94
|
+
'protect-state': { file: 'protect-brain-state.sh', interpreter: 'bash', mode: 'blocking', offBehavior: 'run', stdinBytes: 65536 },
|
|
95
95
|
'learn-capture': { file: 'learn-capture.sh', interpreter: 'bash', mode: 'advisory', offBehavior: 'silence' },
|
|
96
96
|
'learn-flush': { file: 'learn-flush.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'silence' },
|
|
97
97
|
'md-stamp': { file: 'md-stamp.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'silence' },
|
|
@@ -102,7 +102,7 @@ const TABLE = {
|
|
|
102
102
|
// SEPARATE process, SEPARATE cadence) resolves later via `gh run list`. offBehavior 'silence' per
|
|
103
103
|
// the ADR's explicit instruction: this plane observes and advises, it never guards money or
|
|
104
104
|
// honesty on its own — the git-push debt it opens is only ever SURFACED (never gated) by
|
|
105
|
-
// session-start.
|
|
105
|
+
// session-start-core.mjs, and that surfacing already lives under SessionStart's 'partial' contract.
|
|
106
106
|
'signal-watch': { file: 'signal-watch.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'silence' },
|
|
107
107
|
'routing-outcome': { file: 'routing-outcome-capture.mjs', interpreter: 'node', mode: 'advisory', offBehavior: 'run' },
|
|
108
108
|
// The unprompted-speech chokepoint (ADR-040 / DDD-0004). ONE runtime is the sole writer of
|
|
@@ -112,7 +112,7 @@ const TABLE = {
|
|
|
112
112
|
// exit code, and an opted-in lesson BLOCK must propagate as exit 2 (an advisory delivery is exit 0
|
|
113
113
|
// and passes straight through; a spawn error is exit 1, which CC treats as a non-blocking notice).
|
|
114
114
|
// The CC event name is forwarded to the runtime as an extra argv (see runHook's arg plumbing).
|
|
115
|
-
'unprompted-speech': { file: 'unprompted-runtime.mjs', interpreter: 'node', mode: 'blocking', channel: 'unprompted', offBehavior: 'silence' },
|
|
115
|
+
'unprompted-speech': { file: 'unprompted-runtime.mjs', interpreter: 'node', mode: 'blocking', channel: 'unprompted', offBehavior: 'silence', stdinBytes: 65536 },
|
|
116
116
|
// THE LIFECYCLE PLANE (ADR-055 §2, build item 1). Stop was the ONE registered hook that bypassed
|
|
117
117
|
// this table entirely — hooks.json pointed straight at continuation-gate.mjs, so it had no mode,
|
|
118
118
|
// no offBehavior, and no spine resolution, contradicting this file's own `_note` and leaving
|
|
@@ -156,7 +156,7 @@ if (BRAIN_OFF && entry.offBehavior === 'silence') process.exit(0);
|
|
|
156
156
|
// already running, and skip the interpreter entirely only when BOTH prompt intent and project
|
|
157
157
|
// state prove that no advisory can fire. The regex deliberately over-approximates the shell gates:
|
|
158
158
|
// false positives take the established body; false negatives would be a product defect.
|
|
159
|
-
let
|
|
159
|
+
let hookInput = null;
|
|
160
160
|
const GROUND_RELEVANT = /ruvnet|ruflo|ruvector|\brvf\b|agentdb|agenticow|rulake|ruview|rupixel|ruv-fann|agentic-flow|synthlang|dspy|qudag|safla|metaharness|cve-bench|sparc|swarm|claude-flow|pinecone|pgvector|chroma|weaviate|faiss|milvus|qdrant|hnswlib|annoy|vector|langchain|llama|autogen|crew-ai|semantic-kernel|embedding|retrieval|prompt compression|token cost|post-quantum|quantum-resistant|\badr\b|decision|architect|design|plan|spec|refactor|migrat|implement|build|write|add|change|fix|update|deploy|create|enhance|set up|setup|wire|integrate|test|coverage|audit|review|benchmark|lint|scan|debug|optimi|app|feature|service|system|backend|frontend|\bapi\b|module|pipeline|infra|database|schema|workflow|roadmap|milestone|autonomous|unattended|do not stop|keep working|keep going|soak run|harness|quality|readiness|evolve|self-improv|hardening|cheaper|cheap|lower cost|compute arbitrage|cascade|scorecard|score .*repo/i;
|
|
161
161
|
|
|
162
162
|
function projectCanSpeakWithoutPrompt() {
|
|
@@ -173,7 +173,7 @@ function projectCanSpeakWithoutPrompt() {
|
|
|
173
173
|
return false;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
function
|
|
176
|
+
function readHookInput(limit) {
|
|
177
177
|
return new Promise((resolve) => {
|
|
178
178
|
const chunks = [];
|
|
179
179
|
let bytes = 0;
|
|
@@ -194,8 +194,8 @@ function readGroundInput() {
|
|
|
194
194
|
idle.unref?.();
|
|
195
195
|
};
|
|
196
196
|
process.stdin.on('data', (chunk) => {
|
|
197
|
-
if (bytes <
|
|
198
|
-
const kept = chunk.subarray(0,
|
|
197
|
+
if (bytes < limit) {
|
|
198
|
+
const kept = chunk.subarray(0, limit - bytes);
|
|
199
199
|
chunks.push(kept);
|
|
200
200
|
bytes += kept.length;
|
|
201
201
|
}
|
|
@@ -247,15 +247,15 @@ function runHook(file) {
|
|
|
247
247
|
// The unprompted-speech chokepoint uses this to receive the CC event name:
|
|
248
248
|
// `hook-shim.mjs unprompted-speech UserPromptSubmit` → `unprompted-runtime.mjs UserPromptSubmit`.
|
|
249
249
|
const extraArgs = process.argv.slice(3);
|
|
250
|
-
// Forward the ONE resolved OFF snapshot to a 'partial' body (ADR-054 §4). The
|
|
251
|
-
//
|
|
252
|
-
// this shim
|
|
253
|
-
//
|
|
250
|
+
// Forward the ONE resolved OFF snapshot to a 'partial' body (ADR-054 §4). The native SessionStart
|
|
251
|
+
// core also reads the sentinel because the POSIX compatibility launcher and bare installs invoke
|
|
252
|
+
// it outside this shim. Passing the snapshot means the two readings cannot disagree within one
|
|
253
|
+
// invocation if the user flips the switch while the hook is mid-run.
|
|
254
254
|
const env = (BRAIN_OFF && entry.offBehavior === 'partial')
|
|
255
255
|
? { ...process.env, RUVNET_BRAIN_OFF: '1' }
|
|
256
256
|
: process.env;
|
|
257
|
-
const io =
|
|
258
|
-
? { stdio: ['pipe', 'inherit', 'inherit'], input:
|
|
257
|
+
const io = hookInput !== null
|
|
258
|
+
? { stdio: ['pipe', 'inherit', 'inherit'], input: hookInput }
|
|
259
259
|
: { stdio: 'inherit' };
|
|
260
260
|
const r = spawnSync(cmd, [file, ...extraArgs], { ...io, env });
|
|
261
261
|
if (r.error) {
|
|
@@ -287,17 +287,22 @@ function dispatchHook() {
|
|
|
287
287
|
return runHook(fallbackFile);
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
290
|
+
if (entry.stdinBytes) {
|
|
291
|
+
readHookInput(entry.stdinBytes).then((input) => {
|
|
292
|
+
hookInput = input;
|
|
293
|
+
if (hookId === 'ground-ruvnet') {
|
|
294
|
+
let text = input.toString('utf8');
|
|
295
|
+
try {
|
|
296
|
+
const parsed = JSON.parse(text);
|
|
297
|
+
text = parsed?.prompt ?? parsed?.user_prompt ?? parsed?.input ?? text;
|
|
298
|
+
} catch { /* raw/malformed input is classified as-is */ }
|
|
299
|
+
if (!GROUND_RELEVANT.test(String(text)) && !projectCanSpeakWithoutPrompt()) process.exit(0);
|
|
300
|
+
}
|
|
299
301
|
process.exit(dispatchHook());
|
|
300
|
-
}).catch(() =>
|
|
302
|
+
}).catch(() => {
|
|
303
|
+
hookInput = Buffer.alloc(0);
|
|
304
|
+
process.exit(dispatchHook());
|
|
305
|
+
});
|
|
301
306
|
} else {
|
|
302
307
|
process.exit(dispatchHook());
|
|
303
308
|
}
|
|
@@ -9,6 +9,21 @@
|
|
|
9
9
|
|
|
10
10
|
set -uo pipefail
|
|
11
11
|
|
|
12
|
+
# One policy source for both capture and flush. `off` means zero bytes written. `project` keeps the
|
|
13
|
+
# trajectory queue under this project's .swarm directory; `user` preserves the cross-project learner
|
|
14
|
+
# introduced by ADR-0017. Tests and managed hosts may pass an already-resolved snapshot in
|
|
15
|
+
# RUVNET_LEARNING_SCOPE so the two halves cannot disagree during one hook invocation.
|
|
16
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
|
17
|
+
SCOPE="${RUVNET_LEARNING_SCOPE:-}"
|
|
18
|
+
if [ -z "$SCOPE" ] && [ -f "$HERE/runtime-preferences.mjs" ] && command -v node >/dev/null 2>&1; then
|
|
19
|
+
SCOPE=$(node "$HERE/runtime-preferences.mjs" --learning-scope 2>/dev/null) || SCOPE=""
|
|
20
|
+
fi
|
|
21
|
+
case "$SCOPE" in
|
|
22
|
+
off) exit 0 ;;
|
|
23
|
+
user|project) ;;
|
|
24
|
+
*) SCOPE="project" ;;
|
|
25
|
+
esac
|
|
26
|
+
|
|
12
27
|
# BOUNDED READ. An unqualified `read` waits forever on a stdin that is opened and never closed, and
|
|
13
28
|
# an unbounded accumulator turns a large payload into an unbounded regex scan. Claude Code always
|
|
14
29
|
# writes the payload and closes, so neither costs a normal turn — which is exactly why a hook that
|
|
@@ -104,7 +119,11 @@ re_s='"session_id"[[:space:]]*:[[:space:]]*"([^"\]*)"'
|
|
|
104
119
|
# keeping `.` would let a crafted id survive as `..`-shaped debris in a filename for no benefit.
|
|
105
120
|
SID="${SID//[^A-Za-z0-9_-]/}" # a filename COMPONENT, never a path
|
|
106
121
|
[ -n "$SID" ] || SID="default"
|
|
107
|
-
|
|
122
|
+
if [ "$SCOPE" = "user" ]; then
|
|
123
|
+
DIR="$HOME/.cache/ruvnet-brain/learn"
|
|
124
|
+
else
|
|
125
|
+
DIR="$PWD/.swarm/ruvnet-brain-learn"
|
|
126
|
+
fi
|
|
108
127
|
# Owner-only (0700 dir / 0600 file). This queue was 0644 inside a 0755 dir: on macOS every local
|
|
109
128
|
# account is normally in `staff`, so any other user on a shared or corporate machine could read it.
|
|
110
129
|
( umask 077 && mkdir -p "$DIR" ) 2>/dev/null || exit 0
|
|
@@ -147,8 +166,8 @@ if [ "$LINES" -ge "$HEARTBEAT_EVERY" ]; then
|
|
|
147
166
|
LAST=$(cat "$STAMP" 2>/dev/null || echo 0)
|
|
148
167
|
if [ $((NOW - LAST)) -ge 60 ]; then
|
|
149
168
|
echo "$NOW" > "$STAMP" 2>/dev/null || true
|
|
150
|
-
FLUSH="$
|
|
151
|
-
[ -f "$FLUSH" ] && (nohup node "$FLUSH" >/dev/null 2>&1 &) || true
|
|
169
|
+
FLUSH="$HERE/learn-flush.mjs"
|
|
170
|
+
[ -f "$FLUSH" ] && (RUVNET_LEARNING_SCOPE="$SCOPE" nohup node "$FLUSH" >/dev/null 2>&1 &) || true
|
|
152
171
|
fi
|
|
153
172
|
fi
|
|
154
173
|
exit 0
|
|
@@ -13,8 +13,15 @@ import os from 'node:os';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { execFileSync } from 'node:child_process';
|
|
15
15
|
import { readStdinBounded } from './hook-input.mjs';
|
|
16
|
+
import { loadRuntimePreferences } from './runtime-preferences.mjs';
|
|
16
17
|
|
|
17
18
|
const HOME = os.homedir();
|
|
19
|
+
const PROJECT = process.env.RUVNET_BRAIN_PROJECT_DIR || process.cwd();
|
|
20
|
+
const configuredScope = process.env.RUVNET_LEARNING_SCOPE
|
|
21
|
+
|| loadRuntimePreferences({ cwd: PROJECT }).values.learningScope;
|
|
22
|
+
const LEARNING_SCOPE = ['off', 'project', 'user'].includes(configuredScope)
|
|
23
|
+
? configuredScope : 'project';
|
|
24
|
+
if (LEARNING_SCOPE === 'off') process.exit(0);
|
|
18
25
|
|
|
19
26
|
// THE SESSION ID COMES OFF THE PAYLOAD, exactly as it does in learn-capture.sh (fixed 2026-07-27).
|
|
20
27
|
//
|
|
@@ -37,8 +44,12 @@ async function payloadSessionId() {
|
|
|
37
44
|
}
|
|
38
45
|
// A filename COMPONENT, never a path — the payload is untrusted input.
|
|
39
46
|
const SID = ((await payloadSessionId()) || process.env.CLAUDE_SESSION_ID || '').replace(/[^A-Za-z0-9_-]/g, '') || 'default';
|
|
40
|
-
const
|
|
47
|
+
const QUEUE_ROOT = LEARNING_SCOPE === 'user'
|
|
48
|
+
? path.join(HOME, '.cache', 'ruvnet-brain', 'learn')
|
|
49
|
+
: path.join(PROJECT, '.swarm', 'ruvnet-brain-learn');
|
|
50
|
+
const QUEUE = process.env.LEARN_QUEUE || path.join(QUEUE_ROOT, `session-${SID}.jsonl`);
|
|
41
51
|
const RUFLO = path.join(HOME, '.npm-global/bin/ruflo');
|
|
52
|
+
const RUFLO_ENV = { ...process.env, RUFLO_DAEMON_AUTOSTART: '0' };
|
|
42
53
|
const MAX_ACTIONS = 8; // bound the work so SessionEnd stays fast
|
|
43
54
|
|
|
44
55
|
// THE DEADLINE. SessionEnd's registered timeout is 30s (plugin/hooks/hooks.json) and this hook fires
|
|
@@ -100,8 +111,14 @@ for (let i = 0; i < actions.length; i++) {
|
|
|
100
111
|
? ['hooks', 'post-command', '-c', s.action, '-s', 'true']
|
|
101
112
|
: ['hooks', 'post-edit', '-f', s.action, '-s', 'true', '-o', 'session edit'];
|
|
102
113
|
try {
|
|
103
|
-
//
|
|
104
|
-
|
|
114
|
+
// One command, two real Ruflo scopes: project cwd keeps patterns local; HOME retains the
|
|
115
|
+
// cross-project SONA learner for users who explicitly chose `user`.
|
|
116
|
+
execFileSync(RUFLO, args, {
|
|
117
|
+
cwd: LEARNING_SCOPE === 'user' ? HOME : PROJECT,
|
|
118
|
+
env: RUFLO_ENV,
|
|
119
|
+
stdio: 'ignore',
|
|
120
|
+
timeout: Math.min(6000, remaining),
|
|
121
|
+
});
|
|
105
122
|
fed++;
|
|
106
123
|
} catch { /* best-effort — one slow/failed record must not stall session end */ }
|
|
107
124
|
}
|
|
@@ -129,7 +146,7 @@ if (fed > 0 || allDistinct.length === 0) {
|
|
|
129
146
|
console.log(`learn-flush: 0/${actions.length} fed (ruflo hooks failing?) — queue KEPT for retry next session-end`);
|
|
130
147
|
}
|
|
131
148
|
if (process.argv.includes('--sync')) {
|
|
132
|
-
console.log(`learn-flush: fed ${fed}/${actions.length} distinct actions to the
|
|
149
|
+
console.log(`learn-flush: fed ${fed}/${actions.length} distinct actions to the ${LEARNING_SCOPE} learner`
|
|
133
150
|
// Say the deadline out loud when it fires. A budget that silently truncates reads as "that was
|
|
134
151
|
// all there was", which is the same lie as the count cap that preceded it.
|
|
135
152
|
+ (stoppedAt < actions.length ? `; STOPPED at ${stoppedAt}/${actions.length} on the ${DEADLINE_MS}ms deadline` : '')
|