Follow-up to #136 (fixed in 71c2107, v4.0.48-dev). Two things the original diagnosis — mine — got structurally wrong, and one new hazard created by an unrelated upstream fix landing in ruflo. Neither is the release-boundary gap already tracked on #136.
1. The fix hardcodes the opposite direction
#136 read the learner with cwd: SYSTEM_HOME; the fix changed it to cwd: process.cwd(). Both are hardcodes. Neither asks which scope is actually in effect.
scripts/learn-flush.mjs already resolves this properly:
// :27-31
const configuredScope = process.env.RUVNET_LEARNING_SCOPE
|| loadRuntimePreferences({ cwd: PROJECT }).values.learningScope;
const LEARNING_SCOPE = ['off', 'project', 'user'].includes(configuredScope)
? configuredScope : 'project'; // default: project
// :54
const QUEUE_ROOT = LEARNING_SCOPE === 'user'
? path.join(HOME, '.cache', 'ruvnet-brain', 'learn')
: path.join(PROJECT, '.swarm', 'ruvnet-brain-learn');
// :137
cwd: LEARNING_SCOPE === 'user' ? HOME : PROJECT,
So the writer is scope-aware and the reader is not. process.cwd() happens to be correct only because project is the default. Set RUVNET_LEARNING_SCOPE=user and the bug inverts: the flush feeds ~/.claude-flow/neural while the console reads <project>/.claude-flow/neural. Same card, same false positive, opposite direction.
The durable fix is for the console to resolve scope the same way learn-flush.mjs does, so the two agree by construction rather than by coincidence.
2. Now that ruflo's --train is real, the stale card becomes self-clearing
This is the part I would flag first.
ruvnet/ruflo#2940 (--train was a no-op) is fixed — shipped in ruflo v3.38.9, which is current on npm. hooks_intelligence now genuinely calls distillLearning().
health-repair.mjs:198 still runs that command with cwd: HOME:
execFileSync(RUFLO, ['hooks','intelligence','--train'], { cwd: HOME, ... })
Previously this was harmless-but-useless: the remedy did nothing, so the card re-fired and stayed visible. Now the button works. On a default (project-scope) install it will really train the user store, move that store's lastAdaptation to 0s, and the card will disappear — while the learner the operator actually uses is untouched.
That converts a visible false positive into a silent one. A remedy that succeeds against the wrong target is materially harder to catch than one that visibly does nothing, and health-repair.mjs:20 states the discipline it breaks ("Every result is DERIVED from a re-measurement, never asserted from an exit code").
Reproduction — this machine, today
.console-runtime is still serving 4.0.36 (pre-fix), so onboarding-console.mjs:2219 is still cwd: SYSTEM_HOME. RUVNET_LEARNING_SCOPE is unset, so the flush is project-scope by default.
| store |
trajectories |
stats.json mtime |
~/.claude-flow/neural — what the card reads |
1216 |
2026-08-03 17:48 (10.2 days) |
<project>/.claude-flow/neural — where work lands |
10309 |
seconds before the read |
Card text: "last trained 10.1 days ago (1216 trajectories recorded)". .last-flush in the user queue is 1785767503 = 2026-08-03 17:48 — the same minute the user store froze, i.e. the moment scope stopped being user.
The flush itself is healthy — I assumed it had died and was wrong. Verified against a copied queue:
$ LEARN_QUEUE=/tmp/probe.jsonl node scripts/learn-flush.mjs --sync
learn-flush: fed 8/8 distinct actions to the project learner;
36 distinct action(s) deferred to the next flush (queue kept, nothing discarded)
Worth noting for anyone else debugging this: running learn-flush.mjs by hand exits 0 silently, because SID falls back to default, the queue path doesn't exist, and :87 swallows it in a catch. That exit 0 proves nothing — the LEARN_QUEUE override is the only way to actually exercise it.
Also: the 7 session files accumulating in the project queue are not a failure. codex-hook-wrapper.mjs:23 allots learn-flush 2250ms, which fed 8 of 44 actions before deferring the rest. Capture outpaces the drain; nothing is lost.
Versions
Follow-up to #136 (fixed in
71c2107, v4.0.48-dev). Two things the original diagnosis — mine — got structurally wrong, and one new hazard created by an unrelated upstream fix landing in ruflo. Neither is the release-boundary gap already tracked on #136.1. The fix hardcodes the opposite direction
#136 read the learner with
cwd: SYSTEM_HOME; the fix changed it tocwd: process.cwd(). Both are hardcodes. Neither asks which scope is actually in effect.scripts/learn-flush.mjsalready resolves this properly:So the writer is scope-aware and the reader is not.
process.cwd()happens to be correct only becauseprojectis the default. SetRUVNET_LEARNING_SCOPE=userand the bug inverts: the flush feeds~/.claude-flow/neuralwhile the console reads<project>/.claude-flow/neural. Same card, same false positive, opposite direction.The durable fix is for the console to resolve scope the same way
learn-flush.mjsdoes, so the two agree by construction rather than by coincidence.2. Now that ruflo's
--trainis real, the stale card becomes self-clearingThis is the part I would flag first.
ruvnet/ruflo#2940 (
--trainwas a no-op) is fixed — shipped in ruflo v3.38.9, which is current on npm.hooks_intelligencenow genuinely callsdistillLearning().health-repair.mjs:198still runs that command withcwd: HOME:Previously this was harmless-but-useless: the remedy did nothing, so the card re-fired and stayed visible. Now the button works. On a default (
project-scope) install it will really train the user store, move that store'slastAdaptationto 0s, and the card will disappear — while the learner the operator actually uses is untouched.That converts a visible false positive into a silent one. A remedy that succeeds against the wrong target is materially harder to catch than one that visibly does nothing, and
health-repair.mjs:20states the discipline it breaks ("Every result is DERIVED from a re-measurement, never asserted from an exit code").Reproduction — this machine, today
.console-runtimeis still serving 4.0.36 (pre-fix), soonboarding-console.mjs:2219is stillcwd: SYSTEM_HOME.RUVNET_LEARNING_SCOPEis unset, so the flush isproject-scope by default.stats.jsonmtime~/.claude-flow/neural— what the card reads<project>/.claude-flow/neural— where work landsCard text: "last trained 10.1 days ago (1216 trajectories recorded)".
.last-flushin the user queue is1785767503= 2026-08-03 17:48 — the same minute the user store froze, i.e. the moment scope stopped beinguser.The flush itself is healthy — I assumed it had died and was wrong. Verified against a copied queue:
Worth noting for anyone else debugging this: running
learn-flush.mjsby hand exits 0 silently, becauseSIDfalls back todefault, the queue path doesn't exist, and:87swallows it in acatch. That exit 0 proves nothing — theLEARN_QUEUEoverride is the only way to actually exercise it.Also: the 7 session files accumulating in the project queue are not a failure.
codex-hook-wrapper.mjs:23allots learn-flush 2250ms, which fed 8 of 44 actions before deferring the rest. Capture outpaces the drain; nothing is lost.Versions
.console-runtime: 4.0.36--trainfix)