ruvnet-brain 4.6.4 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/bin/install.mjs +6 -50
  3. package/config/practical-rule-catalog.json +22 -22
  4. package/console/app.js +1 -1
  5. package/console/architecture.html +2 -2
  6. package/package.json +1 -2
  7. package/plugin/.claude-plugin/plugin.json +1 -1
  8. package/plugin/.codex-plugin/plugin.json +1 -1
  9. package/plugin/commands/checkpoint.md +22 -40
  10. package/plugin/commands/rnb-brief.md +18 -15
  11. package/plugin/hooks/codex-hooks.json +1 -28
  12. package/plugin/hooks/hook-contracts.json +6 -127
  13. package/plugin/hooks/hooks.json +1 -40
  14. package/plugin/mcp/managed-cli-interface.mjs +0 -56
  15. package/plugin/scripts/agentdb-recall-reader.mjs +29 -9
  16. package/plugin/scripts/agentdb-recall.mjs +8 -14
  17. package/plugin/scripts/brain-footprint.mjs +1 -1
  18. package/plugin/scripts/codex-hook-wrapper.mjs +13 -3
  19. package/plugin/scripts/completion-claim-evidence.mjs +1 -1
  20. package/plugin/scripts/continuity-events.mjs +90 -2
  21. package/plugin/scripts/continuity-hook-policy.mjs +5 -14
  22. package/plugin/scripts/decision-gate.mjs +2 -2
  23. package/plugin/scripts/decision-record.mjs +11 -40
  24. package/plugin/scripts/hook-failures.mjs +16 -0
  25. package/plugin/scripts/hook-shim.mjs +1 -2
  26. package/plugin/scripts/learn-capture.mjs +60 -42
  27. package/plugin/scripts/learning-store.mjs +70 -60
  28. package/plugin/scripts/node-sqlite.mjs +1 -1
  29. package/plugin/scripts/ruflo-bin.mjs +71 -1
  30. package/plugin/scripts/ruflo-memory-hooks.mjs +132 -0
  31. package/plugin/scripts/runtime-preferences.mjs +1 -1
  32. package/plugin/scripts/session-snapshot-hook.mjs +85 -341
  33. package/plugin/scripts/session-start-core.mjs +2 -12
  34. package/plugin/scripts/signal-watch.mjs +1 -1
  35. package/plugin/scripts/substitution-wall.mjs +222 -0
  36. package/plugin/scripts/turn-outcome-capture.mjs +68 -74
  37. package/scripts/codex-host-execution-proof.mjs +1 -1
  38. package/scripts/console-engine.mjs +0 -32
  39. package/scripts/health-repair.mjs +1 -81
  40. package/scripts/hook-qualify-core.mjs +2 -2
  41. package/scripts/model-managed-workflow-service.mjs +9 -10
  42. package/scripts/model-routing-controller.mjs +9 -8
  43. package/scripts/no-silent-substitution.mjs +65 -1
  44. package/scripts/onboarding-console.mjs +8 -23
  45. package/scripts/product-integrity-contract.mjs +1 -1
  46. package/scripts/release-qualification-contract.mjs +9 -27
  47. package/scripts/remedy-registry.mjs +0 -18
  48. package/scripts/wired-check.mjs +1 -5
  49. package/plugin/scripts/continuity-brief.mjs +0 -351
  50. package/plugin/scripts/continuity-journal.mjs +0 -553
  51. package/plugin/scripts/learn-flush.mjs +0 -96
  52. package/plugin/scripts/learning-observation.mjs +0 -51
  53. package/plugin/scripts/learning-queue.mjs +0 -135
  54. package/plugin/scripts/learning-worker-supervisor.mjs +0 -79
  55. package/plugin/scripts/project-capture-queue.mjs +0 -357
  56. package/plugin/scripts/project-memory-enrollment.mjs +0 -203
  57. package/plugin/scripts/project-progression-checkpoint.mjs +0 -145
  58. package/plugin/scripts/project-progression-contract.mjs +0 -496
  59. package/plugin/scripts/project-progression-hook.mjs +0 -233
  60. package/plugin/scripts/project-progression-outbox.mjs +0 -225
  61. package/plugin/scripts/project-progression-producer.mjs +0 -309
  62. package/plugin/scripts/project-progression-reader.mjs +0 -304
  63. package/plugin/scripts/project-progression-session-start.mjs +0 -320
  64. package/plugin/scripts/project-progression-sources.mjs +0 -246
  65. package/plugin/scripts/project-progression-store.mjs +0 -723
  66. package/plugin/scripts/project-progression-suspension.mjs +0 -60
  67. package/plugin/scripts/project-transition-hook.mjs +0 -267
  68. package/plugin/scripts/session-snapshot-contract.mjs +0 -105
  69. package/plugin/scripts/turn-transport-journal.mjs +0 -108
  70. package/scripts/qa/progression-validation-benchmark.mjs +0 -66
  71. 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 learn-flush.mjs computes, by the same rule — the two halves of the flush
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
- // project-transition-hook.mjs dispatches child/failure observations by event name.
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 and project-transition-hook.mjs; native schema and delivery NOT observed; producer completeness NOT verified`,
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 { ContinuityJournal, drain } from '../plugin/scripts/continuity-journal.mjs';
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 exact current receipt row in the adopted canonical store; queued debt fails closed. */
266
- export function commitManagedReceipt(request, receipt, { Journal = ContinuityJournal, drainJournal = drain, read = withProgressionReader } = {}) {
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 [row] = journal.record([event]); assert(row, 'Current workflow receipt was not durably enqueued');
273
- drainJournal(journal, { budgetMs: Math.max(0, Math.min(1000, request.deadline - Date.now())), backoff: [] });
274
- const readback = read(journal.db, (reader) => reader.readContent(CONTINUITY_NAMESPACE, row.key));
275
- assert(readback.ok && readback.value === JSON.stringify(row.event), 'Canonical AgentDB exact receipt readback failed; queued is not complete');
276
- return { durable: true, agentDbCommitted: true, key: row.key, digest: row.digest };
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) {
@@ -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 { ContinuityJournal, drain } from '../plugin/scripts/continuity-journal.mjs';
11
- import { EVENT_SCHEMA } from '../plugin/scripts/continuity-events.mjs';
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
- journal.record([event]);
280
- // Existing outbox is durable before the existing exact-readback AgentDB drainer runs.
281
- const status = drain(journal, { budgetMs: Math.max(0, Math.min(1000, request.deadline - Date.now())), backoff: [] });
282
- return { durable: true, agentDbCommitted: status.remaining === 0, ...status };
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) {
@@ -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
- const violations = audit();
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-observation.mjs';
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
- // compaction survival + session surfacing are filesystem facts, always checkable
498
- const snapshot = inspectSessionSnapshots(projectDir, { now });
499
- const snapshotDetail = snapshot.kind === 'canonical'
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/learn-flush (learningScope) obey the merge.
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 = ['learning:flush', 'learning:train', 'learning:flush-legacy-user'].includes(id) ? observeLearning() : null;
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
- const progressed = id === 'learning:flush-legacy-user'
2629
- ? after.legacyUserKnown && after.legacyUserDepth < learningBefore.legacyUserDepth
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/project-progression-contract.mjs', 'plugin/scripts/project-progression-hook.mjs'], ['tests/unit/project-progression-contract.test.mjs', 'tests/acceptance/cross-host-project-resume.test.mjs'], ['ruvnet-brain-project-progression', 'ruvnet-brain-cross-host-resume'], ['ADR-073', 'DDD-0019']),
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',
@@ -59,14 +59,11 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
59
59
  },
60
60
  {
61
61
  "id": "canonical-learning-capture",
62
- "reason": "Fixed metadata capture, canonical scope, consent, acknowledgement and bounded owned recovery retain privacy and originals",
62
+ "reason": "Fixed metadata capture, canonical scope, consent, exact readback and loud refusal retain privacy; learning is handed straight to ruflo memory store with no local queue",
63
63
  "files": [
64
- "tests/unit/learn-flush-partial-failure.test.mjs",
65
64
  "tests/unit/learn-capture-project-root.test.mjs",
66
65
  "tests/unit/learn-capture-redaction.test.mjs",
67
- "tests/unit/learner-scope-agreement.test.mjs",
68
- "tests/unit/health-repair-flush-learning.test.mjs",
69
- "tests/unit/learning-worker-supervisor.test.mjs"
66
+ "tests/unit/learner-scope-agreement.test.mjs"
70
67
  ]
71
68
  },
72
69
  {
@@ -77,7 +74,7 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
77
74
  {
78
75
  "id": "complete-progression-validation",
79
76
  "reason": "Canonical digest validation retains full-history and serialization semantics",
80
- "files": ["tests/unit/project-progression-contract.test.mjs"]
77
+ "files": []
81
78
  },
82
79
  {
83
80
  "id": "signed-artifacts",
@@ -211,8 +208,6 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
211
208
  "files": [
212
209
  "tests/unit/turn-outcome-capture.test.mjs",
213
210
  "tests/unit/turn-capture-content-privacy.test.mjs",
214
- "tests/integration/project-progression-hook.test.mjs",
215
- "tests/unit/turn-journal-platform.test.mjs",
216
211
  "tests/unit/project-store-resolver.test.mjs"
217
212
  ]
218
213
  },
@@ -287,11 +282,6 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
287
282
  "id": "pending-memory-durability",
288
283
  "reason": "Complete outbox tails replay and accepted pending journal events survive capacity pressure without silent deletion",
289
284
  "files": [
290
- "tests/unit/project-progression-outbox.test.mjs",
291
- "tests/unit/project-progression-durability.test.mjs",
292
- "tests/unit/continuity-customer-regressions.test.mjs",
293
- "tests/unit/progression-outbox-containment.test.mjs",
294
- "tests/unit/transition-pending-notices.test.mjs"
295
285
  ]
296
286
  }
297
287
  ],
@@ -312,16 +302,16 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
312
302
  },
313
303
  {
314
304
  "id": "canonical-learning-recovery",
315
- "reason": "Cross-session recovery and Console evidence agree on the same canonical scope without ratifying tool metadata as instructions",
316
- "files": ["tests/integration/learning-recovery-377.test.mjs", "tests/integration/learning-console-scope.test.mjs"]
305
+ "reason": "Retired 2026-10-12: the learning queue, flush worker and Console queue recovery were deleted (owner ruling: hooks only call rUv's memory tools); learning writes are covered by canonical-learning-capture",
306
+ "files": []
317
307
  },
318
308
  {
319
309
  "id": "canonical-progression-store",
320
310
  "reason": "Actual adopted canonical storage, exact readback, and concurrent session restoration retain consent and complete history",
321
- "files": ["tests/integration/project-progression-concurrent-sessions.test.mjs", "tests/integration/project-progression-reader-identity.test.mjs"],
311
+ "files": [],
322
312
  "platformFiles": {
323
- "linux": ["tests/integration/project-progression-store.test.mjs"],
324
- "macos": ["tests/integration/project-progression-store.test.mjs"]
313
+ "linux": [],
314
+ "macos": []
325
315
  }
326
316
  },
327
317
  {
@@ -372,7 +362,6 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
372
362
  "tests/unit/agentdb-recall.test.mjs",
373
363
  "tests/unit/session-start-core-parity.test.mjs",
374
364
  "tests/unit/learning-replay.test.mjs",
375
- "tests/integration/continuity-journal.test.mjs",
376
365
  "tests/integration/changed-memory-process-probes.test.mjs",
377
366
  "tests/unit/managed-memory-boundary.test.mjs"
378
367
  ]
@@ -381,20 +370,13 @@ export const RELEASE_REQUIREMENTS = Object.freeze({
381
370
  "id": "automatic-memory-transition-durability",
382
371
  "reason": "Reviewed POSIX automatic boundary capture, complete history, consent suspension, first-start recovery and data-only turn replay; real global Ruflo and filesystem prerequisites are required",
383
372
  "files": [
384
- "tests/unit/project-transition-hook.test.mjs",
385
- "tests/unit/turn-transport-security.test.mjs",
386
- "tests/unit/turn-durable-transport.test.mjs",
387
- "tests/integration/automatic-progression-continuation.test.mjs",
388
- "tests/integration/capture-consent-boundary.test.mjs",
389
- "tests/integration/session-start-turn-replay.test.mjs",
390
- "tests/integration/progression-suspension.test.mjs"
373
+ "tests/integration/capture-consent-boundary.test.mjs"
391
374
  ]
392
375
  },
393
376
  {
394
377
  "id": "pending-continuity-capacity",
395
378
  "reason": "POSIX non-root permission refusal, capacity-pressure reporting and recovery preserve all accepted pending continuity events",
396
379
  "files": [
397
- "tests/unit/continuity-journal-bounds.test.mjs"
398
380
  ]
399
381
  }
400
382
  ]
@@ -68,22 +68,6 @@ export const REMEDIES = [
68
68
  // The inverse is restoring it. This is the branch whose absence made the promise a lie.
69
69
  inverse: () => ({ kind: K.RESTORE_MEMORY_BACKUP }),
70
70
  },
71
- {
72
- key: 'learning-flush',
73
- summary: 'drain the capture queue into the learner',
74
- match: (id) => (id === 'learning:flush' ? {} : null),
75
- plan: () => ({ script: 'scripts/health-repair.mjs', args: ['--flush-learning'] }),
76
- // Genuinely additive: it moves already-captured local events into the learner. Declared NONE on
77
- // purpose, and the human string says what a user would actually do instead.
78
- inverse: () => ({ kind: K.NONE, human: 'nothing to reverse — this only adds observations the learner already had queued; learned state can be reset separately' }),
79
- },
80
- {
81
- key: 'learning-legacy-user-flush',
82
- summary: 'drain retained legacy user history into its original home learner',
83
- match: (id) => (id === 'learning:flush-legacy-user' ? {} : null),
84
- plan: () => ({ script: 'scripts/health-repair.mjs', args: ['--flush-legacy-user-learning'] }),
85
- inverse: () => ({ kind: K.NONE, human: 'original queue bytes stay retained; learned observations can be reset separately' }),
86
- },
87
71
  {
88
72
  key: 'learning-train',
89
73
  summary: 'distill canonical observations into structural patterns',
@@ -235,8 +219,6 @@ export function assertRegistryClosure(offeredIds = [], handledUndoKinds = []) {
235
219
  export function sampleIdFor(remedy) {
236
220
  switch (remedy.key) {
237
221
  case 'memory-index': return 'repair:memory-index';
238
- case 'learning-flush': return 'learning:flush';
239
- case 'learning-legacy-user-flush': return 'learning:flush-legacy-user';
240
222
  case 'learning-train': return 'learning:train';
241
223
  case 'distill-fleet': return 'learning:distill-fleet';
242
224
  case 'enable-memory-distillation': return 'enable:memory-distillation';
@@ -104,10 +104,6 @@ const STANDALONE = [
104
104
  ['grounding-turn-replay', 'human-run measurement harness for grounding-turn-gate.mjs (ADR-0030 #1 and shadow #2/#3): replays real transcripts read-only through the same pure functions the hooks call, sharing completion-claim-replay.mjs\'s turn walkers; nothing to schedule'],
105
105
  ['duplicate-gate-replay','human-run tuning harness for plugin/scripts/duplicate-gate.mjs: replays the gate over git history, read-only, when its thresholds are re-tuned; it imports the gate\'s own scoring functions, so there is no second copy to drift and nothing to schedule'],
106
106
  ['sync-commands', 'explicit maintainer alias synchronizer; run deliberately before release, never from a lifecycle hook'],
107
- ['project-progression-checkpoint', 'the body of the shipped `/ruvnet-brain:checkpoint` command '
108
- + '(plugin/commands/checkpoint.md:39; added a7167b6b 2026-09-11). The command host executes that '
109
- + 'instruction, and this check does not scan command markdown — the same shape as '
110
- + '`onboarding-console` below. Recorded 2026-09-11 when wired-check first named it.'],
111
107
  ['handoff-asset', 'client explainer handoff to a named host, run by a human per docs/CLIENT-ASSET-HANDOFF.md '
112
108
  + '(added 32b7b7ca 2026-09-11 with the TriSmart sub-product); nothing may schedule a copy to a client '
113
109
  + 'machine. Surfaced 2026-09-11 the first time wired-check ran with no agent worktrees under '
@@ -953,7 +949,7 @@ export function hookWiringAudit({
953
949
  // below double-counts every OTHER entry in its TABLE as "spawned", because the pass's own predicate
954
950
  // (callerPattern) matches any quoted filename — and every TABLE entry is, by construction, a quoted
955
951
  // filename in hook-shim.mjs's source, dispatched or not. Measured: before this exclusion,
956
- // design-wall.sh, route-dispatch.sh, verify-interface.sh, learn-capture.sh, learn-flush.mjs,
952
+ // design-wall.sh, route-dispatch.sh, verify-interface.sh, learn-capture.sh,
957
953
  // md-stamp.mjs, signal-watch.mjs, routing-outcome-capture.mjs and swarm-slot-recycler.mjs all read
958
954
  // "wired · via spawned by plugin/scripts/hook-shim.mjs" while zero manifest (hooks.json,
959
955
  // codex-hooks.json, this repo's or the user's settings.json) ever dispatches most of their ids —