ruvnet-brain 4.6.4 → 4.7.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.
- package/README.md +1 -1
- package/bin/install.mjs +65 -54
- package/config/practical-rule-catalog.json +22 -22
- package/console/app.js +1 -1
- package/console/architecture.html +2 -2
- package/package.json +1 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.grok-plugin/plugin.json +12 -0
- package/plugin/commands/checkpoint.md +22 -40
- package/plugin/commands/rnb-brief.md +18 -15
- package/plugin/hooks/codex-hooks.json +1 -28
- package/plugin/hooks/grok-hooks.json +95 -0
- package/plugin/hooks/hook-contracts.json +6 -127
- package/plugin/hooks/hooks.json +1 -40
- package/plugin/host-adapters/grok.json +11 -0
- package/plugin/mcp/managed-cli-interface.mjs +0 -56
- package/plugin/scripts/agentdb-recall-reader.mjs +29 -9
- package/plugin/scripts/agentdb-recall.mjs +35 -27
- package/plugin/scripts/automated-run.mjs +42 -0
- package/plugin/scripts/brain-footprint.mjs +1 -1
- package/plugin/scripts/checkpoint-keys.mjs +46 -0
- package/plugin/scripts/codex-hook-wrapper.mjs +13 -3
- package/plugin/scripts/completion-claim-evidence.mjs +1 -1
- package/plugin/scripts/continuation-gate.mjs +6 -0
- package/plugin/scripts/continuity-events.mjs +90 -2
- package/plugin/scripts/continuity-hook-policy.mjs +5 -14
- package/plugin/scripts/decision-gate.mjs +2 -2
- package/plugin/scripts/decision-record.mjs +11 -40
- package/plugin/scripts/grok-hook-adapter.mjs +155 -0
- package/plugin/scripts/hook-failures.mjs +16 -0
- package/plugin/scripts/hook-shim.mjs +1 -2
- package/plugin/scripts/learn-capture.mjs +60 -42
- package/plugin/scripts/learning-store.mjs +70 -60
- package/plugin/scripts/node-sqlite.mjs +1 -1
- package/plugin/scripts/ruflo-bin.mjs +71 -1
- package/plugin/scripts/ruflo-memory-hooks.mjs +204 -0
- package/plugin/scripts/runtime-preferences.mjs +1 -1
- package/plugin/scripts/session-snapshot-hook.mjs +85 -341
- package/plugin/scripts/session-start-core.mjs +2 -12
- package/plugin/scripts/signal-watch.mjs +1 -1
- package/plugin/scripts/substitution-wall.mjs +222 -0
- package/plugin/scripts/turn-outcome-capture.mjs +68 -74
- package/scripts/codex-host-execution-proof.mjs +1 -1
- package/scripts/console-engine.mjs +0 -32
- package/scripts/health-repair.mjs +1 -81
- package/scripts/hook-qualify-core.mjs +2 -2
- package/scripts/model-managed-workflow-service.mjs +9 -10
- package/scripts/model-native-qualification.mjs +17 -5
- package/scripts/model-routing-controller.mjs +9 -8
- package/scripts/model-routing-execution-adapters.mjs +24 -2
- package/scripts/model-weekly-cycle.mjs +13 -12
- package/scripts/no-silent-substitution.mjs +65 -1
- package/scripts/onboarding-console.mjs +8 -23
- package/scripts/product-integrity-contract.mjs +1 -1
- package/scripts/release-qualification-contract.mjs +21 -30
- package/scripts/remedy-registry.mjs +0 -18
- package/scripts/user-model-prompt-hook.mjs +11 -10
- package/scripts/wired-check.mjs +1 -5
- package/plugin/scripts/continuity-brief.mjs +0 -351
- package/plugin/scripts/continuity-journal.mjs +0 -553
- package/plugin/scripts/learn-flush.mjs +0 -96
- package/plugin/scripts/learning-observation.mjs +0 -51
- package/plugin/scripts/learning-queue.mjs +0 -135
- package/plugin/scripts/learning-worker-supervisor.mjs +0 -79
- package/plugin/scripts/project-capture-queue.mjs +0 -357
- package/plugin/scripts/project-memory-enrollment.mjs +0 -203
- package/plugin/scripts/project-progression-checkpoint.mjs +0 -145
- package/plugin/scripts/project-progression-contract.mjs +0 -496
- package/plugin/scripts/project-progression-hook.mjs +0 -233
- package/plugin/scripts/project-progression-outbox.mjs +0 -225
- package/plugin/scripts/project-progression-producer.mjs +0 -309
- package/plugin/scripts/project-progression-reader.mjs +0 -304
- package/plugin/scripts/project-progression-session-start.mjs +0 -320
- package/plugin/scripts/project-progression-sources.mjs +0 -246
- package/plugin/scripts/project-progression-store.mjs +0 -723
- package/plugin/scripts/project-progression-suspension.mjs +0 -60
- package/plugin/scripts/project-transition-hook.mjs +0 -267
- package/plugin/scripts/session-snapshot-contract.mjs +0 -105
- package/plugin/scripts/turn-transport-journal.mjs +0 -108
- package/scripts/qa/progression-validation-benchmark.mjs +0 -66
- package/scripts/session-snapshot-contract.mjs +0 -1
|
@@ -25,15 +25,13 @@ import os from 'node:os';
|
|
|
25
25
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
26
26
|
import { findStores, diagnose } from './memory-doctor.mjs';
|
|
27
27
|
import { distillLearning } from '../plugin/scripts/learning-store.mjs';
|
|
28
|
-
import { readSafe } from '../plugin/scripts/learning-queue.mjs';
|
|
29
28
|
import { learningContext } from '../plugin/scripts/runtime-preferences.mjs';
|
|
30
|
-
import { learningQueueDepth, learningQueueFiles, observeLearning } from '../plugin/scripts/learning-observation.mjs';
|
|
31
29
|
import { resolveRuflo, rufloInvocation } from '../plugin/scripts/ruflo-bin.mjs';
|
|
32
30
|
import { projectDirectory } from '../plugin/scripts/project-identity.mjs';
|
|
33
31
|
import { newestSnapshot, snapshotInventory, MTIME_GRACE_MS } from './snapshot-freshness.mjs';
|
|
34
32
|
|
|
35
33
|
const HOME = os.homedir();
|
|
36
|
-
// The SAME project root
|
|
34
|
+
// The SAME project root the learning hook computes, by the same rule — the two halves of the flush
|
|
37
35
|
// have to agree about which project they mean or they address different queues (issue #104).
|
|
38
36
|
// RESIDUAL of #134: RUVNET_BRAIN_PROJECT_DIR is never set by real hook dispatch on either host, so
|
|
39
37
|
// consult `projectDirectory()` (project-identity.mjs) — the CLAUDE_PROJECT_DIR-with-containment
|
|
@@ -100,82 +98,6 @@ function repairMemory() {
|
|
|
100
98
|
return { ok: true, log: `repaired — integrity ok, ${rowsAfter} entries intact (was ${rowsBefore}). Backup: ${backup.replace(HOME, '~')}`, backup };
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
/**
|
|
104
|
-
* Drain the capture queue into rUv's learner — his tool, not ours.
|
|
105
|
-
*
|
|
106
|
-
* ISSUE #104: this measured one queue and drained another, so it could only ever report "fed 0".
|
|
107
|
-
* The flusher was spawned with NO environment, so inside learn-flush.mjs RUVNET_LEARNING_SCOPE was
|
|
108
|
-
* unset and defaulted to 'project' — it drained `<project>/.swarm/ruvnet-brain-learn/` while THIS
|
|
109
|
-
* function counted `<user>/.cache/ruvnet-brain/learn/`. `before - after` was structurally 0: it
|
|
110
|
-
* could not report truthfully in either direction even on a flush that worked, and the real queue
|
|
111
|
-
* grew forever. Two independent defaults are not an agreement.
|
|
112
|
-
*
|
|
113
|
-
* So: resolve the scope ONCE, from the same policy module learn-flush reads, derive the queue root
|
|
114
|
-
* FROM that scope, pass the scope (and the exact queue file) to the child explicitly, and measure
|
|
115
|
-
* the root that was actually drained.
|
|
116
|
-
*
|
|
117
|
-
* And LOOP. learn-flush feeds at most MAX_ACTIONS distinct actions per invocation and writes the
|
|
118
|
-
* remainder back on purpose (a SessionEnd hook must stay fast), so one call cannot drain a deep
|
|
119
|
-
* queue — the reporter needed 15 rounds for 293 entries. A single call would leave a queue that
|
|
120
|
-
* "flushes" every time and never empties, which is the same lie in slow motion.
|
|
121
|
-
*/
|
|
122
|
-
function flushLearning(legacyUser = false) {
|
|
123
|
-
const original = learningContext({ cwd: PROJECT });
|
|
124
|
-
const context = legacyUser && original.enabled
|
|
125
|
-
? learningContext({ cwd: PROJECT, env: { ...process.env, RUVNET_LEARNING_SCOPE: 'user' } }) : original;
|
|
126
|
-
if (!context.enabled) return { ok: true, noop: true, log: 'learning is switched off for this project — queued evidence is untouched' };
|
|
127
|
-
const { scope, queueDir } = context;
|
|
128
|
-
const bundled = path.resolve(import.meta.dirname, '../plugin/scripts/learn-flush.mjs');
|
|
129
|
-
const flusher = path.join(HOME, '.claude', 'plugins', 'marketplaces', 'ruvnet-brain', 'plugin', 'scripts', 'learn-flush.mjs');
|
|
130
|
-
const local = path.join(PROJECT, 'plugin', 'scripts', 'learn-flush.mjs');
|
|
131
|
-
const script = [bundled, local, flusher].find((file) => fs.existsSync(file));
|
|
132
|
-
if (!script) return { ok: false, log: 'learn-flush.mjs not found — cannot drain the queue' };
|
|
133
|
-
|
|
134
|
-
// Displayed to a human and matched by tests, so it is normalised to forward slashes on every
|
|
135
|
-
// platform. Without this, Windows reports `~\.cache\ruvnet-brain\learn` while macOS and Linux
|
|
136
|
-
// report `~/.cache/ruvnet-brain/learn` — the same location under two spellings, which is the
|
|
137
|
-
// exact defect class this branch has been closing (one fact, two representations).
|
|
138
|
-
const where = queueDir.replace(HOME, '~').split(path.sep).join('/');
|
|
139
|
-
|
|
140
|
-
const depth = () => learningQueueDepth(queueDir);
|
|
141
|
-
try {
|
|
142
|
-
const lock = JSON.parse(readSafe(path.join(queueDir, '.worker-lock'), 4096));
|
|
143
|
-
if (lock.retirementUnconfirmed || (lock.retirementRequired && lock.expires < Date.now())) return { ok: false, log: 'learning recovery is paused: owned worker retirement is unconfirmed; queue fence and original observations retained' };
|
|
144
|
-
} catch { /* Regular safety checks below still decide whether a queue is readable. */ }
|
|
145
|
-
let before;
|
|
146
|
-
try { before = depth(); }
|
|
147
|
-
catch { return { ok: false, log: `cannot read learning queue ${where} safely — evidence is preserved` }; }
|
|
148
|
-
if (!before) return { ok: true, noop: true, log: `nothing queued in ${where} — the learner is already caught up` };
|
|
149
|
-
const env = { ...process.env, RUVNET_BRAIN_PROJECT_DIR: PROJECT, ...(legacyUser ? { RUVNET_LEARNING_SCOPE: 'user', RUVNET_LEGACY_USER_APPLY: '1' } : {}) };
|
|
150
|
-
const deadline = Date.now() + 540_000;
|
|
151
|
-
const stalled = [];
|
|
152
|
-
let previous = before;
|
|
153
|
-
while (previous > 0 && Date.now() < deadline) {
|
|
154
|
-
try {
|
|
155
|
-
execFileSync(process.execPath, [script, '--sync'], { env, cwd: PROJECT, stdio: 'ignore',
|
|
156
|
-
timeout: Math.min(25_000, Math.max(1000, deadline - Date.now())) });
|
|
157
|
-
} catch { stalled.push('bounded worker failed'); break; }
|
|
158
|
-
const next = depth();
|
|
159
|
-
if (next >= previous) { stalled.push(`${next} entries would not feed`); break; }
|
|
160
|
-
previous = next;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const after = depth();
|
|
164
|
-
const fed = before - after;
|
|
165
|
-
if (fed <= 0) {
|
|
166
|
-
// Name the most likely cause instead of shrugging: learn-flush invokes ruflo at a FIXED path,
|
|
167
|
-
// so on a machine with a different npm prefix it feeds nothing and honestly keeps the queue.
|
|
168
|
-
const why = stalled.length ? ` (${stalled.slice(0, 3).join('; ')})` : '';
|
|
169
|
-
const hint = RUFLO ? '' : ' No global ruflo was resolved from the managed prefix or PATH.';
|
|
170
|
-
return { ok: false, log: `fed 0 of ${before} queued events from ${where}${why} — the queue is preserved for retry.${hint}` };
|
|
171
|
-
}
|
|
172
|
-
return {
|
|
173
|
-
ok: true,
|
|
174
|
-
log: `fed ${fed} captured events into the ${scope} learner (queue ${before} → ${after} in ${where})`
|
|
175
|
-
+ (after ? ` — ${after} remain and will drain on the next flush` : ''),
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
101
|
/** One explicit training cycle in the same learner the Console measures. */
|
|
180
102
|
function trainLearning() {
|
|
181
103
|
const context = learningContext({ cwd: PROJECT });
|
|
@@ -304,8 +226,6 @@ function distillFleet() {
|
|
|
304
226
|
}
|
|
305
227
|
|
|
306
228
|
const action = has('--repair-memory') ? repairMemory
|
|
307
|
-
: has('--flush-legacy-user-learning') ? () => flushLearning(true)
|
|
308
|
-
: has('--flush-learning') ? flushLearning
|
|
309
229
|
: has('--train-learning') ? trainLearning
|
|
310
230
|
: has('--distill-fleet') ? distillFleet
|
|
311
231
|
: null;
|
|
@@ -89,11 +89,11 @@ export function fixturesFor(reg, root = REPO) {
|
|
|
89
89
|
const base = all.find((f) => eventOf(reg.host, f.payload) === baseEvent);
|
|
90
90
|
const event = reg.host === 'grok' ? reg.event.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase() : reg.event;
|
|
91
91
|
// Consumer-only Layer 1 coverage: hook-input.mjs accepts the common host envelope;
|
|
92
|
-
//
|
|
92
|
+
// Child/failure observations are dispatched by event name.
|
|
93
93
|
// No unobserved producer-specific child/error fields are invented. Layer 2/public
|
|
94
94
|
// host verification must establish native schema and delivery independently.
|
|
95
95
|
return base ? [{ name: `${reg.event}-adapter-contract-derived`, nativeSchemaObserved: false, nativeDeliveryObserved: false,
|
|
96
|
-
_provenance: `DERIVED adapter-contract envelope from captured ${reg.host} ${baseEvent}; consumer-only mapping in plugin/scripts/hook-input.mjs
|
|
96
|
+
_provenance: `DERIVED adapter-contract envelope from captured ${reg.host} ${baseEvent}; consumer-only mapping in plugin/scripts/hook-input.mjs; native schema and delivery NOT observed; producer completeness NOT verified`,
|
|
97
97
|
payload: { ...base.payload, hook_event_name: event, ...(reg.host === 'grok' ? { hookEventName: event } : {}) } }] : [];
|
|
98
98
|
}
|
|
99
99
|
if (!/ToolUse$/.test(reg.event)) return [];
|
|
@@ -9,9 +9,8 @@ import { runRoutingWorkflow, validateWorkflowRequest, artifactDigest, verifyCont
|
|
|
9
9
|
import { createGuardedWorkflowAdapters, runObservedWorkflowWorker, nativeWorkflowBinaries } from './model-routing-execution-adapters.mjs';
|
|
10
10
|
import { extractFeatures, selectDecision, loadPolicy, loadProfile, applyProfile, loadCatalog } from './model-router-engine.mjs';
|
|
11
11
|
import { validateDispatchDecision, subscriptionEnvironment } from './model-router-dispatch.mjs';
|
|
12
|
-
import {
|
|
12
|
+
import { storeExact } from '../plugin/scripts/ruflo-memory-hooks.mjs';
|
|
13
13
|
import { CONTINUITY_NAMESPACE, EVENT_SCHEMA, redactText } from '../plugin/scripts/continuity-events.mjs';
|
|
14
|
-
import { withProgressionReader } from '../plugin/scripts/project-progression-reader.mjs';
|
|
15
14
|
import { recall } from '../plugin/scripts/agentdb-recall.mjs';
|
|
16
15
|
import { resolveProjectStore } from '../plugin/scripts/project-store-resolver.mjs';
|
|
17
16
|
import { selectPracticalRules, practicalSelectionReceipt } from './practical-rule-selector.mjs';
|
|
@@ -262,18 +261,18 @@ export async function runRegisteredChecker(check, { deadline, signal, env = proc
|
|
|
262
261
|
return result;
|
|
263
262
|
}
|
|
264
263
|
|
|
265
|
-
/** Completion requires the
|
|
266
|
-
export function commitManagedReceipt(request, receipt, {
|
|
264
|
+
/** Completion requires the receipt written through `ruflo memory store` and read back by exact key. */
|
|
265
|
+
export function commitManagedReceipt(request, receipt, { store = storeExact } = {}) {
|
|
267
266
|
const resolved = resolveProjectStore({ projectDir: request.projectRoot, gitTimeoutMs: 1000 });
|
|
268
|
-
const journal = new Journal({ projectRoot: resolved.projectRoot, projectDir: request.projectRoot });
|
|
269
267
|
const event = { schema: EVENT_SCHEMA, kind: 'decision', id: sha(JSON.stringify(receipt)).slice(0, 16),
|
|
270
268
|
at: receipt.at, source: 'model-managed-workflow-service', authoritative: true,
|
|
271
269
|
summary: `Managed workflow ${request.id}: ${receipt.status}`, detail: receipt };
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
270
|
+
const key = `cevt-workflow-${event.id}`;
|
|
271
|
+
const value = JSON.stringify(event);
|
|
272
|
+
const result = store({ db: resolved.canonicalAgentDbPath, namespace: CONTINUITY_NAMESPACE, key, value,
|
|
273
|
+
timeoutMs: Math.max(1, Math.min(6000, request.deadline - Date.now())) });
|
|
274
|
+
assert(result.verified, `Canonical AgentDB exact receipt readback failed: ${result.error || 'value differs'}`);
|
|
275
|
+
return { durable: true, agentDbCommitted: true, key, digest: sha(value) };
|
|
277
276
|
}
|
|
278
277
|
|
|
279
278
|
function validateStoredRegistry(request) {
|
|
@@ -26,8 +26,19 @@ function binaryFor(host) {
|
|
|
26
26
|
return path.join(entry.location.path, 'resources/native-binary', process.platform === 'win32' ? 'claude.exe' : 'claude');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// The get_usage contract was first verified on native 2.1.289. An exact pin broke every hourly weekly
|
|
30
|
+
// cycle once 2.1.296 was installed (4.7.1), so any 2.x at or above that floor is accepted; the
|
|
31
|
+
// experimental response SHAPE is still checked and fails closed below (verifyClaudeAllowance).
|
|
32
|
+
export function supportedClaudeVersion(harnessVersion) {
|
|
33
|
+
const m = /^(\d+)\.(\d+)\.(\d+) \(Claude Code\)$/.exec(String(harnessVersion || ''));
|
|
34
|
+
if (!m) return null;
|
|
35
|
+
const [major, minor, patch] = m.slice(1).map(Number);
|
|
36
|
+
const atLeastFloor = minor > 1 || (minor === 1 && patch >= 289);
|
|
37
|
+
return major === 2 && atLeastFloor ? `${major}.${minor}.${patch}` : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function verifyClaudeAllowance(usage, model, version = '2.1.289') {
|
|
41
|
+
// Native get_usage is explicitly experimental. Unknown shapes fail closed.
|
|
31
42
|
if (usage?.rate_limits_available !== true || usage.rate_limits?.extra_usage?.is_enabled !== false) throw new Error('Native Claude allowance unknown or extra usage enabled');
|
|
32
43
|
const limits = usage.rate_limits;
|
|
33
44
|
for (const key of ['five_hour', 'seven_day']) {
|
|
@@ -40,7 +51,7 @@ export function verifyClaudeAllowance(usage, model) {
|
|
|
40
51
|
for (const limit of limits.limits || []) {
|
|
41
52
|
if (!Number.isFinite(limit.percent) || limit.percent < 0 || limit.percent >= 100 || !['normal', 'warning'].includes(limit.severity)) throw new Error('Native Claude allowance unavailable or unknown');
|
|
42
53
|
}
|
|
43
|
-
return { verified: true, basis:
|
|
54
|
+
return { verified: true, basis: `native-get_usage-${version}`, checkedAt: new Date().toISOString(), reservation: false, raceSafe: false, model };
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
class Transport {
|
|
@@ -189,10 +200,11 @@ async function run(options, inspectOnly) {
|
|
|
189
200
|
} else result.sourceReceipt.nativeSettings.after.serviceTierBasis = 'pre-turn-native-settings-and-fixed-host-configuration';
|
|
190
201
|
}
|
|
191
202
|
} else {
|
|
192
|
-
|
|
203
|
+
const claudeVersion = supportedClaudeVersion(result.harnessVersion);
|
|
204
|
+
if (!claudeVersion) throw new Error('Experimental Claude allowance contract version unsupported');
|
|
193
205
|
const initialized = await request('initialize');
|
|
194
206
|
if (initialized?.session_state !== 'idle') throw new Error('Native Claude session not idle');
|
|
195
|
-
result.sourceReceipt.allowance = verifyClaudeAllowance(await request('get_usage', { skip_behaviors: true }), model);
|
|
207
|
+
result.sourceReceipt.allowance = verifyClaudeAllowance(await request('get_usage', { skip_behaviors: true }), model, claudeVersion);
|
|
196
208
|
// Native initialize.models is the extension's supportedModels() source.
|
|
197
209
|
result.catalogStatus = Array.isArray(initialized.models) ? 'verified' : 'unknown';
|
|
198
210
|
for (const m of initialized.models || []) {
|
|
@@ -7,8 +7,9 @@ import { createRequire } from 'node:module';
|
|
|
7
7
|
import { pathToFileURL } from 'node:url';
|
|
8
8
|
import { performance } from 'node:perf_hooks';
|
|
9
9
|
import { validateDispatchDecision } from './model-router-dispatch.mjs';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { CONTINUITY_NAMESPACE, EVENT_SCHEMA } from '../plugin/scripts/continuity-events.mjs';
|
|
11
|
+
import { storeExact } from '../plugin/scripts/ruflo-memory-hooks.mjs';
|
|
12
|
+
import { resolveProjectStore } from '../plugin/scripts/project-store-resolver.mjs';
|
|
12
13
|
import { collectMacPressure, effectiveAgentRecommendation } from '../plugin/scripts/capacity-aware-parallel-work.mjs';
|
|
13
14
|
|
|
14
15
|
const HASH = /^[a-f0-9]{64}$/;
|
|
@@ -271,15 +272,15 @@ function guardAdapters(adapters, request, budget, now, admission) {
|
|
|
271
272
|
}));
|
|
272
273
|
}
|
|
273
274
|
|
|
274
|
-
export function durableWorkflowReceipt(request, receipt) {
|
|
275
|
-
const journal = new ContinuityJournal({ projectRoot: request.projectRoot });
|
|
275
|
+
export function durableWorkflowReceipt(request, receipt, { store = storeExact } = {}) {
|
|
276
276
|
const event = { schema: EVENT_SCHEMA, kind: 'decision', id: sha(JSON.stringify(receipt)),
|
|
277
277
|
at: receipt.at, source: 'model-routing-controller', authoritative: true,
|
|
278
278
|
summary: `Routing workflow ${request.id}: ${receipt.status}`, detail: receipt };
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
|
|
279
|
+
// Written directly through `ruflo memory store` and proven by exact-key readback; no local outbox.
|
|
280
|
+
const db = resolveProjectStore({ projectDir: request.projectRoot, gitTimeoutMs: 1000 }).canonicalAgentDbPath;
|
|
281
|
+
const result = store({ db, namespace: CONTINUITY_NAMESPACE, key: `cevt-routing-${event.id.slice(0, 24)}`, value: JSON.stringify(event),
|
|
282
|
+
timeoutMs: Math.max(1, Math.min(6000, request.deadline - Date.now())) });
|
|
283
|
+
return { durable: result.verified, agentDbCommitted: result.verified, ...(result.error ? { error: result.error } : {}) };
|
|
283
284
|
}
|
|
284
285
|
|
|
285
286
|
function validateAcceptance(request, acceptance) {
|
|
@@ -13,8 +13,30 @@ import { readCodexAllowance } from './native-subscription-usage.mjs';
|
|
|
13
13
|
const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
14
14
|
const blocked = reason => Object.assign(new Error(reason), { safetyBlocked: true });
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// The launcher config is retired: an owner who removed it gets the standard install path, else a PATH
|
|
17
|
+
// lookup. A binary that exists nowhere throws ENOENT (as before), never a path that cannot run. Never
|
|
18
|
+
// recreate the config here (install.mjs refreshInstalledModelLaunchers would reinstall the terminal wrappers).
|
|
19
|
+
function findNativeBinary(name, standard, env) {
|
|
20
|
+
const isFile = file => { try { return fs.statSync(file).isFile(); } catch { return false; } };
|
|
21
|
+
if (isFile(standard)) return standard;
|
|
22
|
+
const exts = process.platform === 'win32' ? ['.exe', '.cmd', ''] : [''];
|
|
23
|
+
for (const dir of String(env.PATH || env.Path || '').split(path.delimiter).filter(Boolean)) {
|
|
24
|
+
for (const ext of exts) { const candidate = path.join(dir, name + ext); if (isFile(candidate)) return candidate; }
|
|
25
|
+
}
|
|
26
|
+
throw Object.assign(new Error(`native ${name} binary not found at ${standard} or on PATH`), { code: 'ENOENT' });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function nativeWorkflowBinaries(home = os.homedir(), env = process.env) {
|
|
30
|
+
let text;
|
|
31
|
+
try { text = fs.readFileSync(path.join(home, '.cache/ruvnet-brain/model-routing/terminal-launcher-config.json'), 'utf8'); }
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code !== 'ENOENT') throw error;
|
|
34
|
+
return {
|
|
35
|
+
claude: findNativeBinary('claude', path.join(home, '.npm-global/bin/claude'), env),
|
|
36
|
+
codex: findNativeBinary('codex', path.join(home, '.local/bin/codex'), env)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const config = JSON.parse(text);
|
|
18
40
|
return { claude: config.realClaude, codex: config.realCodex };
|
|
19
41
|
}
|
|
20
42
|
|
|
@@ -100,20 +100,21 @@ export function canonicalTextReleases(bytes) {
|
|
|
100
100
|
function discoveryDue(state, now) { const checked = Date.parse(state?.checkedAt);
|
|
101
101
|
return !Number.isFinite(checked) || checked > now || now - checked >= WEEK_MS;
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
// READ-ONLY status for the per-prompt hook (4.7.1). It never creates, claims or launches anything:
|
|
104
|
+
// the prompt hook used to launch a detached cycle on every prompt, which spawned native claude/codex.
|
|
105
|
+
// The weekly cycle runs only from the scheduled LaunchAgent (com.ruvnet.goldie-weekly, goldie-weekly.sh).
|
|
106
|
+
export function weeklyCycleStatus({ routerDir = path.join(os.homedir(), '.claude', 'model-router'), now = Date.now() } = {}) {
|
|
104
107
|
try {
|
|
105
|
-
|
|
106
|
-
fs.mkdirSync(routerDir, { recursive: true, mode: 0o700 }); const state = json(routerDir, STATE_FILE, 1048576);
|
|
108
|
+
const state = json(routerDir, STATE_FILE, 1048576);
|
|
107
109
|
const reviewRequired = !!state?.pendingReleases?.length;
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
} catch (e) { return { status: 'blocked', launched: false, reason: e.message.slice(0, 240), reviewRequired: false }; }
|
|
110
|
+
const last = json(routerDir, 'weekly-cycle-last-attempt.json');
|
|
111
|
+
if (['failed', 'qualification-pending'].includes(last?.status)) return { status: 'blocked', checkedAt: state?.checkedAt, reviewRequired,
|
|
112
|
+
reason: `last scheduled run ${last.status} at ${last.checkedAt}: ${String(last.reason || 'no reason recorded').slice(0, 200)}` };
|
|
113
|
+
if (reviewRequired) return { status: 'review-pending', checkedAt: state.checkedAt, reviewRequired: true };
|
|
114
|
+
if (discoveryDue(state, now)) return { status: 'due', checkedAt: state?.checkedAt, reviewRequired: false };
|
|
115
|
+
if (state?.removedPublicReleases?.length) return { status: 'blocked', checkedAt: state.checkedAt, reviewRequired: false, reason: 'Public release removal requires native access verification; policy retained' };
|
|
116
|
+
return { status: 'current', checkedAt: state.checkedAt, reviewRequired: false };
|
|
117
|
+
} catch (e) { return { status: 'blocked', reason: e.message.slice(0, 240), reviewRequired: false }; }
|
|
117
118
|
}
|
|
118
119
|
export async function runWeeklyCycle({ routerDir = path.join(os.homedir(), '.claude', 'model-router'), now = Date.now(), stage = runCycleStage,
|
|
119
120
|
env = process.env, maxMs = 900000, fetchImpl = fetch, claimToken = null } = {}) {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import fs from 'node:fs';
|
|
29
29
|
import path from 'node:path';
|
|
30
30
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
|
+
import { substitutionVerdict, inScope, codeOf, message } from '../plugin/scripts/substitution-wall.mjs';
|
|
31
32
|
|
|
32
33
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
33
34
|
|
|
@@ -155,8 +156,71 @@ export function audit(root = ROOT) {
|
|
|
155
156
|
return violations;
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
// ── CLASS 2: STORAGE / QUEUE / JOURNAL (2026-10-12) ─────────────────────────────────────────────
|
|
160
|
+
// The package-name rule above said "none" on a 3.38 GB outbox, queues and replay workers in front of
|
|
161
|
+
// `ruflo memory store`: storage was not in CAPABILITIES, and spawning the `ruflo` binary counted as
|
|
162
|
+
// "rUv is used". This class is behaviour-based, shares ONE detector with the PreToolUse policy
|
|
163
|
+
// (plugin/scripts/substitution-wall.mjs), and is a RATCHET over config/state-files.json:
|
|
164
|
+
// · a file the detector refuses must be in `substitutionBaseline` (known debt, signals recorded) or
|
|
165
|
+
// `substitutionAllow` (owner-approved). A NEW one fails; a baseline file that GAINS a signal fails.
|
|
166
|
+
// · every module that appends to a file must be registered in `stateFiles` with its bound (or an
|
|
167
|
+
// explicit `bound: null`), and the count of unbounded ones may not rise above `maxUnbounded`.
|
|
168
|
+
export const STATE_CONFIG = 'config/state-files.json';
|
|
169
|
+
const APPEND = /\b(?:appendFileSync|appendFile|createWriteStream)\b|\bO_APPEND\b|flags?\s*:\s*['"]a\+?['"]/;
|
|
170
|
+
const BOUND = /\bMAX_\w*(?:BYTES|LINES|RECORDS|ROWS|ENTRIES|AGE\w*)\b|\bmax(?:Bytes|Lines|Records|Rows|Entries)\b|\brotat\w+|\bcompact\w*|\btruncateSync\b|\bprune\w*/;
|
|
171
|
+
|
|
172
|
+
function scopedFiles(root) {
|
|
173
|
+
return ['plugin/scripts', 'scripts'].flatMap((d) => (fs.existsSync(path.join(root, d)) ? walkAll(path.join(root, d)) : []))
|
|
174
|
+
.map((f) => path.relative(root, f).split(path.sep).join('/')).filter(inScope).sort();
|
|
175
|
+
}
|
|
176
|
+
function walkAll(dir, out = []) {
|
|
177
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
178
|
+
if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name) && !e.name.startsWith('.')) walkAll(path.join(dir, e.name), out); }
|
|
179
|
+
else out.push(path.join(dir, e.name));
|
|
180
|
+
}
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Every module that appends to a file: its literal targets and the first bound token in its code (or null). */
|
|
185
|
+
export function stateFileInventory(root = ROOT) {
|
|
186
|
+
return scopedFiles(root).flatMap((rel) => {
|
|
187
|
+
const code = codeOf(fs.readFileSync(path.join(root, rel), 'utf8'), rel);
|
|
188
|
+
if (!APPEND.test(code)) return [];
|
|
189
|
+
const targets = [...new Set([...code.matchAll(/['"`/]([\w.-]+\.(?:jsonl|ndjson|log))['"`]/g)].map((m) => m[1]))].sort();
|
|
190
|
+
return [{ module: rel, targets, bound: BOUND.exec(code)?.[0] ?? null }]; // bound = a grep-detected token, NOT proof it bounds the target
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function readStateConfig(root = ROOT) {
|
|
195
|
+
try { return JSON.parse(fs.readFileSync(path.join(root, STATE_CONFIG), 'utf8')); } catch { return {}; }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function storageAudit(root = ROOT, config = readStateConfig(root)) {
|
|
199
|
+
const violations = []; const notes = [];
|
|
200
|
+
const allow = (config.substitutionAllow || []).map((a) => a.path);
|
|
201
|
+
const baseline = new Map((config.substitutionBaseline || []).map((b) => [b.path, b]));
|
|
202
|
+
for (const rel of scopedFiles(root)) {
|
|
203
|
+
const v = substitutionVerdict({ path: rel, content: fs.readFileSync(path.join(root, rel), 'utf8'), allow });
|
|
204
|
+
const known = baseline.get(rel);
|
|
205
|
+
if (v.verdict === 'refuse' && !known) violations.push({ file: rel, capability: 'storage/queue/journal', pkg: 'ruflo memory', why: message(rel, v).split('\n')[0], fix: `Call ${v.tool}; delete the transport.` });
|
|
206
|
+
else if (v.verdict === 'refuse' && v.signals.some((s) => !known.signals.includes(s))) violations.push({ file: rel, capability: 'storage/queue/journal', pkg: 'ruflo memory', why: `known debt GREW: signals ${v.signals.join(',')} vs baseline ${known.signals.join(',')}`, fix: 'Shrink it toward one ruflo call; never grow it.' });
|
|
207
|
+
else if (known && v.verdict !== 'refuse') notes.push(`${rel} no longer refuses — remove it from substitutionBaseline (the ratchet tightens).`);
|
|
208
|
+
}
|
|
209
|
+
const registered = new Map((config.stateFiles || []).map((s) => [s.module, s]));
|
|
210
|
+
const inventory = stateFileInventory(root);
|
|
211
|
+
for (const s of inventory) if (!registered.has(s.module)) violations.push({ file: s.module, capability: 'state-file ratchet', pkg: STATE_CONFIG, why: `appends to ${s.targets.join(', ') || 'a file'} and is not registered with a bound`, fix: `Register it in ${STATE_CONFIG} stateFiles (bound or explicit null), or store via ruflo memory.` });
|
|
212
|
+
const unbounded = [...registered.values()].filter((s) => s.bound === null && inventory.some((i) => i.module === s.module)).length;
|
|
213
|
+
if (unbounded > (config.maxUnbounded ?? 0)) violations.push({ file: STATE_CONFIG, capability: 'state-file ratchet', pkg: STATE_CONFIG, why: `${unbounded} unbounded append targets > maxUnbounded ${config.maxUnbounded ?? 0}`, fix: 'Bound the new one, or remove an old one first.' });
|
|
214
|
+
for (const m of registered.keys()) if (!inventory.some((i) => i.module === m)) notes.push(`${m} no longer appends — remove it from stateFiles.`);
|
|
215
|
+
return { violations, notes, unbounded };
|
|
216
|
+
}
|
|
217
|
+
|
|
158
218
|
function main() {
|
|
159
|
-
|
|
219
|
+
if (process.argv.includes('--inventory')) { console.log(JSON.stringify(stateFileInventory(), null, 2)); process.exit(0); }
|
|
220
|
+
const storage = storageAudit();
|
|
221
|
+
for (const n of storage.notes) console.log(`ℹ ${n}`);
|
|
222
|
+
console.log(`storage/queue/journal: ${(readStateConfig().substitutionBaseline || []).length} known debt file(s), ${storage.unbounded} unbounded append target(s) registered.`);
|
|
223
|
+
const violations = [...audit(), ...storage.violations];
|
|
160
224
|
console.log('no-silent-substitution — is any local code impersonating a real rUv tool?\n');
|
|
161
225
|
if (!violations.length) {
|
|
162
226
|
console.log('✅ none. Every RuvNet capability this repo names is either genuinely used or openly disclosed as a hand-roll.');
|
|
@@ -46,7 +46,6 @@ import { effectivePrices, loadLabelledRows, MIN_LABELS, OUTCOMES } from './metah
|
|
|
46
46
|
import { utilization } from './router-utilization.mjs';
|
|
47
47
|
import { loadCatalog, detectProvider, frontierFor } from './model-catalog.mjs';
|
|
48
48
|
import { providerAvailability } from './provider-availability.mjs';
|
|
49
|
-
import { inspectSessionSnapshots } from './session-snapshot-contract.mjs';
|
|
50
49
|
import { learnings } from './learnings.mjs';
|
|
51
50
|
import { gatesSurvey } from './gates.mjs';
|
|
52
51
|
// The write-safety primitives, borrowed rather than re-implemented. See saveConfig for why.
|
|
@@ -71,7 +70,7 @@ import {
|
|
|
71
70
|
saveOpenRouterCredential,
|
|
72
71
|
loadRuntimePreferences,
|
|
73
72
|
} from '../plugin/scripts/runtime-preferences.mjs';
|
|
74
|
-
import { observeLearning as observeScopedLearning } from '../plugin/scripts/learning-
|
|
73
|
+
import { observeLearning as observeScopedLearning } from '../plugin/scripts/learning-store.mjs';
|
|
75
74
|
import { applyNightlyChoice, nightlyStatus } from './nightly-controller.mjs';
|
|
76
75
|
// One canonical answer to "which directory is this, and have I counted it already?" — shared with
|
|
77
76
|
// the PreCompact snapshot producer (#85) and with memory-doctor's root scan (#107).
|
|
@@ -494,20 +493,9 @@ function probeMemory(projectDir, { now = Date.now() } = {}) {
|
|
|
494
493
|
const stores = memoryStores(projectDir);
|
|
495
494
|
const db = stores.scored;
|
|
496
495
|
const probes = { stores };
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
? 'a fresh versioned PreCompact snapshot is present in .swarm/agentdb-sessions.jsonl'
|
|
501
|
-
: snapshot.kind === 'legacy'
|
|
502
|
-
? 'a fresh supported Ruflo session snapshot is present (legacy path; migration recommended)'
|
|
503
|
-
: snapshot.kind === 'malformed'
|
|
504
|
-
? 'snapshot files were found but none matched the supported schema'
|
|
505
|
-
: snapshot.kind !== 'absent'
|
|
506
|
-
? 'the newest supported snapshot is stale'
|
|
507
|
-
: 'no supported PreCompact snapshot found for this project yet';
|
|
508
|
-
probes.compactionSurvival = snapshot.fresh
|
|
509
|
-
? { status: 'ok', detail: snapshotDetail, artifact: snapshot.kind }
|
|
510
|
-
: { status: 'warn', detail: snapshotDetail, artifact: snapshot.kind };
|
|
496
|
+
// Compaction survival is a ruflo write at PreCompact (project-state-auto-* + exact readback); a refusal
|
|
497
|
+
// reaches the hook-failures summary. It is not re-probed at render.
|
|
498
|
+
probes.compactionSurvival = { status: 'notTested', detail: 'PreCompact stores a project-state-auto-* checkpoint through ruflo and reads it back; failures appear in the SessionStart hook-failures summary' };
|
|
511
499
|
probes.sessionSurfacing = sessionHookExists() ? { status: 'ok', detail: 'the global SessionStart hook surfaces project state at launch' } : { status: 'warn', detail: 'no SessionStart recall hook found' };
|
|
512
500
|
// recall quality: honestly NOT probed at render (a true probe needs an embedding query; left for an explicit deep test)
|
|
513
501
|
probes.recallQuality = { status: 'notTested', detail: 'not checked this session — a real recall probe needs an embedding round-trip, which render deliberately avoids' };
|
|
@@ -776,7 +764,7 @@ function gatherInventory() {
|
|
|
776
764
|
/**
|
|
777
765
|
* Keys a project-level <project>/.swarm/ruvnet-brain-settings.json overrides FOR THE RUNTIME THAT
|
|
778
766
|
* READS IT. runtime-preferences merges that file over the user-level choices, and route-cheap, the
|
|
779
|
-
* managed-CLI gate (routing, qeFleet) and learn-capture
|
|
767
|
+
* managed-CLI gate (routing, qeFleet) and learn-capture (learningScope) obey the merge.
|
|
780
768
|
* The console saves user-level values only, so without this a project seeded by "Apply these choices
|
|
781
769
|
* to new projects" silently ignored every later change made here (RNBC QA 2026-10-01). Only keys whose
|
|
782
770
|
* consumer honours the project file are reported — advocacy, autoApply and provider are read at user
|
|
@@ -2618,18 +2606,15 @@ function apply(ids) {
|
|
|
2618
2606
|
const undoToken = journalUndo(undoSpec);
|
|
2619
2607
|
phaseMs.undoJournalMs += elapsedMs(journalStartedAt);
|
|
2620
2608
|
const remedyStartedAt = performance.now();
|
|
2621
|
-
const learningBefore =
|
|
2609
|
+
const learningBefore = id === 'learning:train' ? observeLearning() : null;
|
|
2622
2610
|
const res = runNode(plan.exec.script, args, learningBefore ? {
|
|
2623
2611
|
cwd: learningBefore.projectDir,
|
|
2624
2612
|
env: { ...process.env, RUVNET_BRAIN_PROJECT_DIR: learningBefore.projectDir },
|
|
2625
2613
|
} : {});
|
|
2626
2614
|
if (res.ok && learningBefore) {
|
|
2627
2615
|
const after = observeLearning();
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
: id === 'learning:flush'
|
|
2631
|
-
? after.queueKnown && after.queueDepth < learningBefore.queueDepth
|
|
2632
|
-
: learningBefore.statusKnown && after.statusKnown && after.patterns > learningBefore.patterns;
|
|
2616
|
+
// `ruflo memory distill run` owns the patterns; progress here is a known, readable learner afterwards.
|
|
2617
|
+
const progressed = learningBefore.statusKnown && after.statusKnown;
|
|
2633
2618
|
if (!progressed) {
|
|
2634
2619
|
res.ok = false;
|
|
2635
2620
|
res.log += `\nNo measured learning progress: queue ${after.queueDir}; learner ${after.learnerCwd}. Reload to inspect the remaining evidence.`;
|
|
@@ -53,7 +53,7 @@ export const PRODUCT_INTEGRITY_OBLIGATIONS = Object.freeze([
|
|
|
53
53
|
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']),
|
|
54
54
|
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']),
|
|
55
55
|
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']),
|
|
56
|
-
make('S-11', 'Project continuity is complete and host-neutral', 'ProductIntegrityCase', ['HostConvergence', 'RefreshLifecycle'], ['plugin/scripts/
|
|
56
|
+
make('S-11', 'Project continuity is complete and host-neutral', 'ProductIntegrityCase', ['HostConvergence', 'RefreshLifecycle'], ['plugin/scripts/ruflo-memory-hooks.mjs', 'plugin/scripts/session-snapshot-hook.mjs'], ['tests/integration/ruflo-memory-hooks.test.mjs'], ['ruvnet-brain-project-progression', 'ruvnet-brain-cross-host-resume'], ['ADR-073', 'DDD-0019']),
|
|
57
57
|
make('S-12', 'RuvNet capability claims are bound to live evidence before delivery', 'ProductIntegrityCase', ['HostConvergence', 'PublicVerification'], [
|
|
58
58
|
'plugin/scripts/capability-inventory-receipt.mjs',
|
|
59
59
|
'plugin/scripts/capability-claim-evidence.mjs',
|