Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ npx github:sparkling/ruflo-source-patch adr-reindex install # adds /adr-re
npx github:sparkling/ruflo-source-patch verify-interface install # reopen ruvnet-brain's unopenable PreToolUse gate (#12). RETIRED as of ruvnet-brain 3.2.9 (auto-retires; see ADR-010)
npx github:sparkling/ruflo-source-patch mcp-prefix install # rewrite bundled mcp__claude-flow__* refs to mcp__plugin_ruflo-core_ruflo__* — dead under plugin loading (#2685)
npx github:sparkling/ruflo-source-patch design-wall install # scope ruvnet-brain's design-grade commit gate to its own repo — it fires on ANY repo's README otherwise (ruvnet-brain#17)
npx github:sparkling/ruflo-source-patch memory-health install # fix the Onboarding Console's memory-health card scoring the WRONG project (background refresh spawned with the plugin's own cwd)
```

### Keep it live (actions add `run | check`)
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ Actions: `install` · `uninstall` · `status`
|--------|---------------|----------|
| **`mcp-prefix`** | **The plugins' own tools are dead under plugin loading.** Every `ruflo` plugin bundles skills/agents/commands/hooks that name the MCP tools `mcp__claude-flow__*`. That prefix resolves **only** when the server is registered *standalone* under the key `claude-flow` ([#2206](https://github.com/ruvnet/ruflo/issues/2206)). Used **as a plugin** (the marketplace path), Claude Code namespaces the plugin's bundled server, so the same tools are exposed as `mcp__plugin_ruflo-core_ruflo__*`. Per Claude Code's own MCP docs, *"a hook matcher written against the bare server key … never fires for a plugin-bundled server,"* so the bundled `allowed-tools` globs grant nothing and prompt tool names name tools that don't exist. The platform **won't** bridge it ([anthropics/claude-code#29360](https://github.com/anthropics/claude-code/issues/29360) and [#15145](https://github.com/anthropics/claude-code/issues/15145) are both *closed as not planned*). Measured: **3,482 refs across 474 files in ~30 packages**; only `ruflo-core` ships a server, so the plugin-namespaced name is uniformly `mcp__plugin_ruflo-core_ruflo__*`. Rewrites the bare prefix to it. Correct where it takes effect (the files load only when the plugin is enabled, and then that prefix resolves), inert where it doesn't. It does **not** touch the CLI init generators that emit the same prefix into *your project* files, where that prefix is right for standalone-registered projects, so it's genuinely environment-dependent and out of scope | [#2685](https://github.com/ruvnet/ruflo/issues/2685) |
| **`design-wall`** | **A visual-design gate that gates everyone else's repos too.** `ruvnet-brain`'s `design-wall.sh` blocks `git commit` on a staged `README.md`/`explainer/`/`console/` file until a fresh design-grade stamp is recorded, a sound idea for ruvnet-brain's OWN visual surfaces, applied to every other repository on the machine as well, since `${CLAUDE_PROJECT_DIR:-.}` is never checked for identity before requiring the ritual. Measured: blocked a plain-markdown README commit in an unrelated CLI tool's repo. Reads the project's git origin and requires it to actually name `ruvnet-brain`/`stuinfla` before any stamp is required; ruvnet-brain's own commits are still gated exactly as before | [stuinfla/ruvnet-brain#17](https://github.com/stuinfla/ruvnet-brain/issues/17) |
| **`memory-health`** | **The Onboarding Console's memory-health card scores the wrong project.** The console's "Your memory, proven" card is correct on the very first request: `gatherMemory(cwd)` checks the real launch directory's `.swarm/memory.db`. But every read after that is served from an on-disk cache, refreshed by a **detached child process** that `kickRefresh()` spawns with `cwd: REPO`, the plugin's OWN install directory, not the project being served. `REPO` has no project memory store, so the refresh genuinely reports "Liveness: fail" for the wrong project and writes that into the cache, which is then served to every subsequent request. Measured: a project with a real, actively-written 122MB `.swarm/memory.db` scored 33/100 with "this project has no memory store yet". The console never calls `process.chdir()`, so reading the live `process.cwd()` at spawn time (instead of the hardcoded `REPO`) is enough; no argument needs threading through anything | [stuinfla/ruvnet-brain (memory-health card)](https://github.com/stuinfla/ruvnet-brain) |

### Script targets

Expand Down Expand Up @@ -1200,6 +1201,7 @@ this machine*, which is the only form of the question that can be acted on.
| [ruvnet-brain#12](https://github.com/stuinfla/ruvnet-brain/issues/12) **fixed in 3.2.9, target retired** | `verify-interface.sh`'s PreToolUse gate was **unopenable**: its tool regex swallowed any hyphenated binary name (`ruflo-source-patch …` → `ruflo …`) and matched inside plain English prose (`another ruflo process is writing` → `ruflo process is`), while the documented `RUVNET_SKIP_INTERFACE_CHECK=1` override was read from the hook's own environment, where a caller could never set it. **Upstream shipped its own complete rewrite in v3.2.9** (commit `bfc2d36`): real JSON parsing and a command-position-anchored matcher with a working override | `verify-interface` (retired, ADR-010) |
| [ruvnet-brain#13](https://github.com/stuinfla/ruvnet-brain/issues/13) **fixed in 3.2.9** | The same hook parsed its JSON payload with a **regex**, and `[^"]*` could not cross a quote, so a command containing an escaped `"` was **truncated at the first one**. `bash -c "ruflo memory search"` reached the gate as `bash -c \` and ran unchecked. **Fixed by the same v3.2.9 rewrite**: the payload is now parsed as real JSON | `verify-interface` (retired, ADR-010) |
| [ruvnet-brain#17](https://github.com/stuinfla/ruvnet-brain/issues/17) | `design-wall.sh`'s commit gate never checks **which repository** it is running in before requiring a design-grade stamp. A plain README commit in an entirely unrelated repo trips the identical visual-design ritual meant for ruvnet-brain's own explainer/console surfaces. Reads the project's git origin and requires it to actually name `ruvnet-brain`/`stuinfla` first | `design-wall` |
| ruvnet-brain (memory-health card, filed alongside this patch) | The Onboarding Console's background cache-refresh child spawns with `cwd: REPO` (the plugin's own install directory) instead of the server's actual launch directory, so the memory-health card scores the WRONG project's `.swarm/memory.db` on every request after the first. Reads the live `process.cwd()` at spawn time instead (the console never calls `process.chdir()`, so the two are always identical) | `memory-health` |
| [#2633](https://github.com/ruvnet/ruflo/issues/2633) | Unbounded daemon proliferation. `.claude-flow`/`.swarm` state and the daemon dedup lock anchored to raw `process.cwd()` | `cwd`, `daemon`, `cleanup` |
| [#2640](https://github.com/ruvnet/ruflo/issues/2640) | `ruflo init` bundle duplicates plugin-provided skills/commands/agents (100% / 97% overlap) | `dedupe-bundle` |
| [#2638](https://github.com/ruvnet/ruflo/issues/2638) | `ruflo init` (CLAUDE.md) and `codex init` (AGENTS.md) generate divergent instruction files | `dual-codex-claude` |
Expand Down
6 changes: 6 additions & 0 deletions bin/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { adrReindexCommand } from '../lib/adr-reindex/commands.mjs';
import { verifyInterfaceCommand } from '../lib/verify-interface/commands.mjs';
import { mcpPrefixCommand } from '../lib/mcp-prefix/commands.mjs';
import { designWallCommand } from '../lib/design-wall/commands.mjs';
import { memoryHealthCommand } from '../lib/memory-health/commands.mjs';

const ACTIONS = new Set(['install', 'init', 'uninstall', 'remove', 'status', 'run', 'check']);
// `plugin-only` is the current name (it does more than dedupe a bundle now: strips the plugin-duplicated
Expand All @@ -75,6 +76,9 @@ const PLUGIN_PATCH_TARGETS = {
// ruvnet-brain again, a different script: its design-grade commit gate never checks which repo
// it is running in before demanding a visual design ritual for a plain README.md commit.
'design-wall': designWallCommand,
// ruvnet-brain a THIRD time: its Onboarding Console's background cache refresh spawns with the
// plugin's own cwd instead of the server's, so the memory-health card scores the wrong project.
'memory-health': memoryHealthCommand,
};

function usage() {
Expand Down Expand Up @@ -104,6 +108,8 @@ Plugin patches (ruvnet-brain) (actions: install | uninstall | status)
${pad('')} with a documented override that cannot work (stuinfla/ruvnet-brain#12)
${pad('design-wall')}its design-grade commit gate never checks which repo it's running in —
${pad('')} an unrelated repo's plain README.md commit trips the same visual-design wall
${pad('memory-health')}its Onboarding Console scores the PLUGIN's own dir's memory store, not
${pad('')} your project's — a cache-refresh child spawns with the wrong cwd

Plugin patches (all ruflo plugins) (actions: install | uninstall | status)
${pad('mcp-prefix')}bundled skills/agents name tools \`mcp__claude-flow__*\`, which never resolve
Expand Down
83 changes: 83 additions & 0 deletions docs/adr/ADR-025-memory-health-kickrefresh-inherits-server-cwd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# ADR-025: memory-health: kickRefresh's cache-refresh child inherits the server's cwd

**Status**: accepted
**Date**: 2026-07-24
**Deciders**: Henrik Pettersen
**Tags**: patch-target, plugin, ruvnet-brain

## Context

`ruvnet-brain` ships an Onboarding Console (`scripts/onboarding-console.mjs`, served via
`/brain-console` and aliases) whose "Your memory, proven" card scores a project's memory-store
health across five weighted dimensions (liveness, coverage, recall quality, compaction survival,
session surfacing). A `fail` on any tested dimension caps the whole score at <=49.

Measured live (2026-07-24): a project with a genuine, 122MB, actively-written
`.swarm/memory.db` (confirmed by `stat`, and separately confirmed healthy via the AgentDB/ruflo
memory bridge: 10,875 entries, 16,390 patterns learned) scored 33/100. The liveness probe reported
status `fail`, detail "this project has no memory store (.swarm/memory.db) yet".

The card is correct on the very first request: `startServer({ cwd = process.cwd() })` captures the
real launch directory, and `gatherMemory(cwd)` checks `path.join(cwd, '.swarm/memory.db')` against
it. But every subsequent read is served from an on-disk cache
(`~/.claude/ruvnet-brain/state-cache.json` and siblings), refreshed by a **detached child process**
that `kickRefresh()` spawns at most once per 15 seconds:

```js
function kickRefresh() {
const now = Date.now();
if (now - LAST_REFRESH_KICK < 15000) return;
LAST_REFRESH_KICK = now;
try {
const child = spawn(process.execPath, [SELF, '--refresh-cache'],
{ detached: true, stdio: 'ignore', cwd: REPO });
child.unref();
} catch { /* best-effort */ }
}
```

`REPO` (`path.dirname(__dirname)`, the plugin's own install directory) is hardcoded as the child's
`cwd`. The child was spawned with `cwd: REPO`, so the `--refresh-cache` CLI branch's call to
`gatherState(process.cwd())` resolves to the plugin's own directory, never the project being
served. `REPO` has no project `.swarm/memory.db`, so `gatherMemory()` falls back to scoring `REPO`
itself: a genuine "no store" result, for the wrong project, written straight into the on-disk cache
and served to every request thereafter (including the very next page load), then re-derived every
15 seconds by the next kick. Verified against the live server process (bound cwd: the correct
project), the live `/api/state` response (wrong: reports the plugin's own directory), and the
on-disk cache file (also wrong, byte-identical shape to what the console rendered).

## Decision

Read the live `process.cwd()` at spawn time instead of the hardcoded `REPO` constant:

```js
const child = spawn(process.execPath, [SELF, '--refresh-cache'],
{ detached: true, stdio: 'ignore', cwd: process.cwd() });
```

This is sound because the console's server process never calls `process.chdir()` anywhere in the
file (grepped, zero hits), so `process.cwd()` read at the exact moment `kickRefresh()` fires is
always identical to the cwd the server was launched with: the same value `startServer()`'s own
default parameter and the `--serve`/`--print-state` CLI branches already use. No argument needs to
be threaded through `kickRefresh()`'s signature or either of its two call sites
(`startServer()`'s listen callback, and `serveCached()`'s warm-cache branch).

Single literal edit, applied wherever `onboarding-console.mjs` is found installed. Measured: only
under the marketplace checkout's `scripts/` on this machine today. Neither `plugin/scripts/` nor
any cache copy ships this file, unlike `verify-interface.sh`/`design-wall.sh`; `discover()` still
checks all three shapes so a future layout change is caught rather than silently missed.

## Consequences

### Positive

- The memory-health card scores the ACTUAL project the console was launched for, on every request,
not just the first.
- No behavior change to anything else `REPO` is used for in this file (SBOM path, script-runner
invocations, gate surveys); only the one spawn call inside `kickRefresh()` is touched.

### Negative

- None identified. The fix strictly narrows an existing bug's blast radius; it does not change any
documented behavior. `discover()`'s `plugin/scripts/` and cache-copy checks are speculative
(nothing to patch there today) and cost only an `fs.existsSync` each.
53 changes: 53 additions & 0 deletions lib/memory-health/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# `memory-health`

[← ruflo-source-patch](../../README.md)

The Onboarding Console's memory-health card, scoring the wrong project.

Patches the **`ruvnet-brain`** plugin's `scripts/onboarding-console.mjs`, the local server behind
`/brain-console` (aka `/rvbc`, `/rvcb`, `/ruvnet-brain:configure`). Its "Your memory, proven" card
reported a project's `.swarm/memory.db` as missing, "Liveness: fail", capping the score at 33/100,
on a project with a real, 122MB, actively-written store.

## The bug

`gatherMemory(cwd)` scores whichever project the console server was launched from. That much is
correct on the very first request. But every read after that is served from an on-disk cache
(`~/.claude/ruvnet-brain/state-cache.json` and friends), refreshed by a **detached child process**
that `kickRefresh()` spawns at most once per 15 seconds:

```js
const child = spawn(process.execPath, [SELF, '--refresh-cache'],
{ detached: true, stdio: 'ignore', cwd: REPO });
```

`REPO` is `path.dirname(__dirname)`: the **plugin's own install directory**, not the project being
served. The refresh child's `--refresh-cache` branch calls `gatherState(process.cwd())`, which
resolves to `REPO` because that is the `cwd` it was spawned with. `REPO` has no project
`.swarm/memory.db` of its own, so the health check genuinely fails, for the wrong project, and
that result is written into the cache and served to every subsequent request, re-derived every 15s
by the next kick.

## The fix

The console's server process never calls `process.chdir()` anywhere, so `process.cwd()` read at
the moment `kickRefresh()` fires is always identical to the cwd the server was launched with. No
argument needs to be threaded through anything:

```js
const child = spawn(process.execPath, [SELF, '--refresh-cache'],
{ detached: true, stdio: 'ignore', cwd: process.cwd() });
```

The refresh child now inherits the real project directory, exactly like the server's own
`--serve`/`--print-state` CLI paths already do.

## Usage

```bash
npx github:sparkling/ruflo-source-patch memory-health install
npx github:sparkling/ruflo-source-patch memory-health status
npx github:sparkling/ruflo-source-patch memory-health uninstall
```

See [ADR-025](../../docs/adr/ADR-025-memory-health-kickrefresh-inherits-server-cwd.md).
8 changes: 8 additions & 0 deletions lib/memory-health/commands.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Dispatch for the `memory-health` target — fixes the ruvnet-brain Onboarding Console's
// background cache refresh scoring the WRONG project's memory store (see patcher.mjs for what is
// broken). The install/uninstall/status flow and the shared composition engine live in
// ../plugin-command.mjs (ADR-020).

import { runPluginCommand } from '../plugin-command.mjs';

export const memoryHealthCommand = (action) => runPluginCommand('memory-health', action);
Loading