moflo 4.10.2 → 4.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,22 @@
6
6
 
7
7
  ---
8
8
 
9
+ ## Runtime Target: Claude Code Only
10
+
11
+ **MoFlo targets Anthropic's Claude Code exclusively** — the CLI, IDE extensions (VS Code, JetBrains), and the web app at claude.ai/code. It is not a Claude Desktop integration and is never installed into Claude Desktop's config tree.
12
+
13
+ | Tool | Status | Config paths moflo touches |
14
+ |------|--------|----------------------------|
15
+ | Claude Code (CLI, IDE extensions, web) | Sole supported target | `<project>/.mcp.json`, `<project>/.claude/settings.json`, `<project>/.moflo/`, `<project>/moflo.yaml` |
16
+ | Claude Desktop (the macOS/Windows app) | **Out of scope — never** | None — moflo neither reads nor writes Claude Desktop config |
17
+ | Other MCP-capable clients (Cline, Continue, etc.) | Unsupported, may incidentally work | None — bug reports require Claude Code reproduction |
18
+
19
+ **Never** introduce a code path, search list, fixture, or doc that reads from or writes to `~/.claude/claude_desktop_config.json`, `~/Library/Application Support/Claude/`, or `%APPDATA%/Claude/`. Those are Claude **Desktop** paths. Including them in moflo's MCP-config search list caused issue #1126: a parseable Claude Desktop preferences file outranked a malformed project `.mcp.json`, masking the real failure and routing the auto-fixer down a no-op branch.
20
+
21
+ The one ambiguous-looking path is Claude Code's user-level config at `~/.claude.json` (where `claude mcp add` writes). MoFlo doesn't author or rewrite that file either; the project-local `.mcp.json` written by `flo init` is the canonical surface moflo owns end-to-end.
22
+
23
+ ---
24
+
9
25
  ## Getting Started
10
26
 
11
27
  ### Installation
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  ## TL;DR
10
10
 
11
- MoFlo makes your AI coding assistant remember what it learns, check what it knows before exploring files, and get smarter over time — all automatically. Install it, run `flo init`, restart your AI client, and everything just works: your docs and code are indexed on session start so the AI can search them instantly, gates prevent the AI from wasting tokens on blind exploration, task outcomes feed back into routing so it picks the right agent type next time, and context depletion warnings tell you when to start a fresh session. No configuration, no API keys, no cloud services — it all runs locally on your machine.
11
+ MoFlo makes Claude Code remember what it learns, check what it knows before exploring files, and get smarter over time — all automatically. Install it, run `flo init`, restart Claude Code, and everything just works: your docs and code are indexed on session start so Claude can search them instantly, gates prevent Claude from wasting tokens on blind exploration, task outcomes feed back into routing so it picks the right agent type next time, and context depletion warnings tell you when to start a fresh session. No configuration, no API keys, no cloud services — it all runs locally on your machine.
12
12
 
13
13
  ## Quickstart
14
14
 
@@ -17,7 +17,7 @@ npm install --save-dev moflo
17
17
  flo init
18
18
  ```
19
19
 
20
- Restart Claude Code (or your MCP client). That's it — memory, indexing, gates, and routing are all active.
20
+ Restart Claude Code. That's it — memory, indexing, gates, and routing are all active.
21
21
 
22
22
  Or — just ask Claude to install MoFlo into your project and initialize it!
23
23
 
@@ -44,7 +44,7 @@ MoFlo makes deliberate choices so you don't have to:
44
44
  - **Task registration before agents** — Sub-agents can't spawn until work is tracked. Prevents runaway agent proliferation.
45
45
  - **Learned routing** — Task outcomes feed back into the routing system automatically. No manual configuration needed — it gets smarter with use.
46
46
  - **Incremental indexing** — Guidance and code map indexes run on every session start but skip unchanged files. Fast after the first run.
47
- - **Built for Claude Code, works with others** We develop and test exclusively with Claude Code. The MCP tools, memory system, and hooks are client-independent and should work with any MCP-capable AI client, but Claude Code is the only tested target.
47
+ - **Claude Code is the only target** — MoFlo is built and shipped for Anthropic's Claude Code (CLI, IDE extensions, web). It is **not** a Claude Desktop integration: nothing reads from or writes to `~/.claude/claude_desktop_config.json` or `%APPDATA%/Claude/`, and adding paths there is always a bug. The MCP tools, memory system, and hooks could in principle work with any MCP-capable client, but Claude Code is the *only* surface we author for, test against, or accept bug reports on.
48
48
  - **GitHub-oriented** — The `/flo` skill, PR automation, and issue tracking are built around GitHub. With Claude's help, you can adapt them to your own issue tracker and source control system.
49
49
  - **Cross-platform** — Works identically on macOS, Linux, and Windows.
50
50
 
@@ -185,7 +185,7 @@ MoFlo automatically indexes three types of content on every session start, so yo
185
185
 
186
186
  ### How it works
187
187
 
188
- 1. **Session start hook** — When your AI client starts a new session, MoFlo's `SessionStart` hook launches the indexers sequentially in a single background process. This runs silently — you can start working immediately.
188
+ 1. **Session start hook** — When Claude Code starts a new session, MoFlo's `SessionStart` hook launches the indexers sequentially in a single background process. This runs silently — you can start working immediately.
189
189
  2. **Incremental** — Each indexer tracks file modification times. Only files that changed since the last index run are re-processed. The first run on a large codebase may take a minute or two; subsequent runs typically finish in under a second.
190
190
  3. **Embedding generation** — Guidance chunks are embedded using MiniLM-L6-v2 (384 dimensions, WASM). These vectors are stored in the SQLite memory database and used for semantic search.
191
191
  4. **No blocking** — The indexers run in the background and don't block your session from starting. You can begin working immediately.
@@ -211,9 +211,9 @@ flo memory refresh # Reindex everything + rebuild embeddings + vacuum
211
211
 
212
212
  ### Why this matters
213
213
 
214
- Without auto-indexing, your AI assistant starts every session with a blank slate — it doesn't know what documentation exists, where code lives, or what tests cover which files. It resorts to Glob/Grep exploration, which burns tokens and context window on rediscovery.
214
+ Without auto-indexing, Claude Code starts every session with a blank slate — it doesn't know what documentation exists, where code lives, or what tests cover which files. It resorts to Glob/Grep exploration, which burns tokens and context window on rediscovery.
215
215
 
216
- With auto-indexing, the AI can search semantically ("how does auth work?") and get relevant documentation chunks ranked by similarity, or ask "where is the user model defined?" and get a direct answer from the code map — all without touching the filesystem.
216
+ With auto-indexing, Claude can search semantically ("how does auth work?") and get relevant documentation chunks ranked by similarity, or ask "where is the user model defined?" and get a direct answer from the code map — all without touching the filesystem.
217
217
 
218
218
  ## The Gate System
219
219
 
@@ -261,7 +261,7 @@ You can also disable individual hooks in `.claude/settings.json` by removing the
261
261
 
262
262
  ## The `/flo` Skill
263
263
 
264
- Inside your AI client, the `/flo` (or `/fl`) slash command drives GitHub issue execution:
264
+ Inside Claude Code, the `/flo` (or `/fl`) slash command drives GitHub issue execution:
265
265
 
266
266
  ```
267
267
  /flo <issue> # Full process (research → implement → test → PR)
@@ -272,7 +272,7 @@ Inside your AI client, the `/flo` (or `/fl`) slash command drives GitHub issue e
272
272
  /flo -n <issue> # Normal mode (default, single agent, no swarm)
273
273
  ```
274
274
 
275
- For full options and details, type `/flo` with no arguments — your AI client will display the complete skill documentation. Also available as `/fl`.
275
+ For full options and details, type `/flo` with no arguments — Claude Code will display the complete skill documentation. Also available as `/fl`.
276
276
 
277
277
  ### Epic handling
278
278
 
@@ -313,7 +313,7 @@ flo epic reset 42 # Reset state for re-run
313
313
 
314
314
  ### What are spells?
315
315
 
316
- Spells are declarative YAML automations composed of pluggable step commands. They exist because shell scripts drift, ad-hoc prompts aren't reproducible, and CI/CD pipelines are the wrong tool for local automation. A spell is **deterministic** (same inputs → same steps), **reviewable** (a YAML file you read like a recipe), and **replayable** (re-cast it tomorrow and it behaves the same). Spells run from the CLI (`flo spell cast`), from an MCP tool call inside your AI client, or on a schedule.
316
+ Spells are declarative YAML automations composed of pluggable step commands. They exist because shell scripts drift, ad-hoc prompts aren't reproducible, and CI/CD pipelines are the wrong tool for local automation. A spell is **deterministic** (same inputs → same steps), **reviewable** (a YAML file you read like a recipe), and **replayable** (re-cast it tomorrow and it behaves the same). Spells run from the CLI (`flo spell cast`), from an MCP tool call inside Claude Code, or on a schedule.
317
317
 
318
318
  ### How do they work?
319
319
 
@@ -425,7 +425,7 @@ For full configuration (`scheduler:` block in `moflo.yaml`), event types, and th
425
425
 
426
426
  ### Building spells with the `/spell-builder` skill
427
427
 
428
- Inside your AI client, use the `/spell-builder` skill to create, edit, and validate spell definitions interactively. The skill understands the full spell schema and available step commands, so you can describe what you want in natural language and it will generate the YAML:
428
+ Inside Claude Code, use the `/spell-builder` skill to create, edit, and validate spell definitions interactively. The skill understands the full spell schema and available step commands, so you can describe what you want in natural language and it will generate the YAML:
429
429
 
430
430
  ```
431
431
  /spell-builder # Start the spell builder
@@ -507,7 +507,7 @@ flo diagnose --json # JSON output for CI/automation
507
507
 
508
508
  #### `flo healer` — Health Check
509
509
 
510
- `flo healer` runs 28 parallel health checks against your environment and reports pass/warn/fail for each:
510
+ `flo healer` runs 38 parallel health checks against your environment and reports pass/warn/fail for each:
511
511
 
512
512
  | Check | What it verifies |
513
513
  |-------|-----------------|
@@ -549,7 +549,8 @@ flo diagnose --json # JSON output for CI/automation
549
549
  | Missing config file | Runs `config init` to generate defaults |
550
550
  | Status line not wired | Adds `statusLine` config block to `.claude/settings.json` |
551
551
  | Stale daemon lock | Removes stale `.moflo/daemon.lock` and restarts daemon |
552
- | MCP server not configured | Runs `claude mcp add moflo` to register the server |
552
+ | Malformed `.mcp.json` (e.g. unescaped Windows paths) | Backs up the broken file as `.mcp.json.malformed-<ts>`, regenerates a clean one, verifies it parses (#1126) |
553
+ | MCP server missing from a valid `.mcp.json` | Runs `claude mcp add moflo` to register the server |
553
554
  | Claude Code CLI missing | Installs `@anthropic-ai/claude-code` globally |
554
555
  | Zombie processes | Kills orphaned MoFlo processes (tracked + OS-level scan) |
555
556
 
@@ -658,7 +659,7 @@ These are the backend systems that hooks and commands interact with.
658
659
  | **EWC++ Consolidation** | Elastic Weight Consolidation that prevents catastrophic forgetting | New learning doesn't overwrite patterns from earlier sessions | Yes |
659
660
  | **Session Persistence** | Stop hook exports session metrics; SessionStart hook restores prior state | Patterns learned on Monday are available on Friday | Yes |
660
661
  | **Status Line** | Live dashboard showing git branch, session state, memory stats, MCP status | At-a-glance visibility into what MoFlo is doing | Yes |
661
- | **MCP Tool Server** | 100+ MCP tools for memory, hooks, coordination, spells, swarm, etc. (schemas deferred by default) | Enables AI clients to interact with MoFlo programmatically | Yes (deferred) |
662
+ | **MCP Tool Server** | 80+ MCP tools for memory, hooks, coordination, spells, swarm, etc. (schemas deferred by default) | Lets Claude Code call MoFlo functionality directly | Yes (deferred) |
662
663
 
663
664
  ### Systems (available but off by default)
664
665
 
@@ -670,11 +671,11 @@ These are the backend systems that hooks and commands interact with.
670
671
 
671
672
  ## The Two-Layer Task System
672
673
 
673
- MoFlo doesn't replace your AI client's task system — it wraps it. Your client (Claude Code, Cursor, or any MCP-capable tool) handles spawning agents and running code. MoFlo adds a coordination layer on top that handles memory, routing, and learning.
674
+ MoFlo doesn't replace Claude Code's task system — it wraps it. Claude Code handles spawning agents and running code. MoFlo adds a coordination layer on top that handles memory, routing, and learning.
674
675
 
675
676
  ```
676
677
  ┌──────────────────────────────────────────────────┐
677
- YOUR AI CLIENT (Execution Layer)
678
+ CLAUDE CODE (Execution Layer)
678
679
  │ Spawns agents, runs code, streams output │
679
680
  │ TaskCreate → Agent → TaskUpdate → results │
680
681
  ├──────────────────────────────────────────────────┤
@@ -688,14 +689,14 @@ Here's how a typical task flows through both layers:
688
689
 
689
690
  1. **MoFlo routes** — Before work starts, MoFlo analyzes the prompt and recommends an agent type and model tier via hook or MCP tool.
690
691
  2. **MoFlo gates** — Before an agent can spawn, MoFlo verifies that memory was searched and a task was registered. This prevents blind exploration.
691
- 3. **Your client executes** — The actual agent runs through your client's native task system. MoFlo doesn't manage the agent — your client handles execution, output, and completion.
692
+ 3. **Claude Code executes** — The actual agent runs through Claude Code's native task system. MoFlo doesn't manage the agent — Claude Code handles execution, output, and completion.
692
693
  4. **MoFlo learns** — After the agent finishes, MoFlo records what worked (or didn't) in its memory database. Successful patterns feed into future routing.
693
694
 
694
- The key insight: **your client handles execution, MoFlo handles knowledge.** Your client is good at spawning agents and running code. MoFlo is good at remembering what happened, routing to the right agent, and ensuring prior knowledge is checked before exploring from scratch.
695
+ The key insight: **Claude Code handles execution, MoFlo handles knowledge.** Claude Code is good at spawning agents and running code. MoFlo is good at remembering what happened, routing to the right agent, and ensuring prior knowledge is checked before exploring from scratch.
695
696
 
696
- For complex work, MoFlo structures tasks into waves — a research wave discovers context, then an implementation wave acts on it — with dependencies tracked through both the client's task system and MoFlo's coordination layer. The full integration pattern is documented in `.claude/guidance/moflo-claude-swarm-cohesion.md`.
697
+ For complex work, MoFlo structures tasks into waves — a research wave discovers context, then an implementation wave acts on it — with dependencies tracked through both Claude Code's task system and MoFlo's coordination layer. The full integration pattern is documented in `.claude/guidance/moflo-claude-swarm-cohesion.md`.
697
698
 
698
- The `/flo` skill ties both systems together for GitHub issues — driving the full process (research → enhance → implement → test → simplify → PR) with your client's agents for execution and MoFlo's memory for continuity.
699
+ The `/flo` skill ties both systems together for GitHub issues — driving the full process (research → enhance → implement → test → simplify → PR) with Claude Code's agents for execution and MoFlo's memory for continuity.
699
700
 
700
701
  ### Intelligent Agent Routing
701
702
 
@@ -358,10 +358,67 @@ function fireAndForget(cmd, args, label) {
358
358
  }
359
359
  }
360
360
 
361
+ // Cross-platform sync sleep — Atomics.wait parks the thread at the OS level
362
+ // without burning CPU (same primitive as src/cli/shared/utils/atomic-file-
363
+ // write.ts:131). Used by stopDaemon's liveness polling between graceful and
364
+ // forced termination so we never unlink the lockfile while the daemon is
365
+ // still running.
366
+ const STOP_SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
367
+ function sleepSyncMs(ms) {
368
+ Atomics.wait(STOP_SLEEP_BUF, 0, 0, ms);
369
+ }
370
+
371
+ // PID liveness check. EPERM means the process exists but is owned by another
372
+ // user — treat as alive (matches the canonical isAlive in process-manager.mjs
373
+ // after #1061; the prior `catch { return false; }` falsely reported foreign-
374
+ // owned daemons as dead and let the lockfile be unlinked under them).
375
+ //
376
+ // Linux zombie handling: on Linux, `kill(pid, 0)` returns success for zombie
377
+ // processes (exited but not yet reaped by their parent). A zombie can't write
378
+ // to the DB, hold locks, or do anything else stopDaemon cares about — treating
379
+ // it as alive exhausts the kill budget polling a corpse, then preserves the
380
+ // lockfile under a dead process. Production never hits this (the daemon is
381
+ // detached and reaped by init/systemd within ~ms), but a misbehaving parent
382
+ // can keep a daemon zombified, and the launcher's vitest harness reproduces
383
+ // the case deterministically (#1083 CI failure on ubuntu-latest). Read
384
+ // /proc/<pid>/stat (fixed-format, cheap) and treat 'Z' as dead.
385
+ function isDaemonPidAlive(pid) {
386
+ try {
387
+ process.kill(pid, 0);
388
+ } catch (err) {
389
+ return err && err.code === 'EPERM';
390
+ }
391
+ if (process.platform === 'linux') {
392
+ try {
393
+ const stat = readFileSync(`/proc/${pid}/stat`, 'utf-8');
394
+ // Format: "pid (comm) state ..." — comm can contain spaces/parens, so
395
+ // parse from the LAST ')' to skip it safely.
396
+ const lastParen = stat.lastIndexOf(')');
397
+ if (lastParen !== -1 && stat.charAt(lastParen + 2) === 'Z') return false;
398
+ } catch (err) {
399
+ // ENOENT = pid vanished between kill(0) and the read — already dead.
400
+ if (err && err.code === 'ENOENT') return false;
401
+ // Anything else (e.g. /proc unavailable) — keep the kill(0) verdict.
402
+ }
403
+ }
404
+ return true;
405
+ }
406
+
361
407
  // Stop the daemon recorded in `lockFile` (if any) without restarting. Used by
362
408
  // the upgrade flow before any DB work — the daemon must not be holding old
363
409
  // path resolution in memory, and a concurrent sql.js flush would clobber the
364
- // cherry-picked rows. Returns true when a live PID was actually killed.
410
+ // cherry-picked rows. Returns true when a live PID was confirmed dead (or the
411
+ // PID was already gone when we read the lockfile).
412
+ //
413
+ // Escalation mirrors src/cli/commands/daemon.ts:killBackgroundDaemon so the
414
+ // launcher's upgrade path and `flo daemon stop` behave identically: graceful
415
+ // signal → wait → liveness check → force kill. The prior implementation sent
416
+ // bare `process.kill(pid, 'SIGTERM')` on every platform, which on Windows
417
+ // either silently force-kills or fails entirely depending on the process; in
418
+ // either case the catch swallowed the outcome and the lockfile was unlinked.
419
+ // The daemon (if it survived) then re-wrote the lockfile with its stale PID +
420
+ // pre-upgrade version, defeating the section-3a-pre version-skew recovery and
421
+ // leaving the statusline stuck on `📊 ?` until manual `flo daemon restart`.
365
422
  //
366
423
  // Section 4's `hooks.mjs session-start` spawn is responsible for starting a
367
424
  // fresh daemon under the current code; this function intentionally does not.
@@ -372,11 +429,61 @@ function stopDaemon(lockFile) {
372
429
  const lock = JSON.parse(readFileSync(lockFile, 'utf-8'));
373
430
  if (typeof lock?.pid === 'number' && lock.pid > 0) stalePid = lock.pid;
374
431
  } catch { /* malformed lock — fall through to unlink */ }
375
- if (stalePid !== null) {
376
- try { process.kill(stalePid, 'SIGTERM'); } catch { /* already dead */ }
432
+
433
+ let killed = false;
434
+ if (stalePid !== null && isDaemonPidAlive(stalePid)) {
435
+ // Graceful signal — platform-aware. On Windows, `process.kill(pid, 'SIGTERM')`
436
+ // silently force-kills (skipping the daemon's shutdown handlers that flush
437
+ // sql.js + release lock cleanly), so use bare `taskkill` (no /F) for a
438
+ // close-event signal.
439
+ try {
440
+ if (process.platform === 'win32') {
441
+ execFileSync('taskkill', ['/PID', String(stalePid)], { windowsHide: true, timeout: 5000 });
442
+ } else {
443
+ process.kill(stalePid, 'SIGTERM');
444
+ }
445
+ } catch { /* signal/spawn failed — fall through to liveness poll + force */ }
446
+
447
+ // Poll for death up to 3s. The daemon's shutdown handler does a final
448
+ // sql.js dump + lock release, which under load can take ~1s.
449
+ const gracefulDeadline = Date.now() + 3000;
450
+ while (Date.now() < gracefulDeadline) {
451
+ if (!isDaemonPidAlive(stalePid)) { killed = true; break; }
452
+ sleepSyncMs(100);
453
+ }
454
+
455
+ // Force-kill if still alive.
456
+ if (!killed) {
457
+ try {
458
+ if (process.platform === 'win32') {
459
+ execFileSync('taskkill', ['/F', '/T', '/PID', String(stalePid)], { windowsHide: true, timeout: 5000 });
460
+ } else {
461
+ process.kill(stalePid, 'SIGKILL');
462
+ }
463
+ } catch { /* dead or unreachable */ }
464
+ // Short grace period for OS reap.
465
+ const forceDeadline = Date.now() + 1000;
466
+ while (Date.now() < forceDeadline) {
467
+ if (!isDaemonPidAlive(stalePid)) { killed = true; break; }
468
+ sleepSyncMs(100);
469
+ }
470
+ }
471
+
472
+ if (!killed) {
473
+ // Daemon survived both signals. Leave the lockfile in place so the next
474
+ // session can see the stale PID and retry — unlinking now would let the
475
+ // surviving daemon re-write the lockfile with its stale PID + version,
476
+ // perpetuating the loop this fix exists to break.
477
+ emitWarning(`stopDaemon: PID ${stalePid} did not exit after SIGTERM+force-kill; lockfile preserved`);
478
+ return false;
479
+ }
480
+ } else if (stalePid !== null) {
481
+ // PID was in the lockfile but the process is already gone — clean unlink.
482
+ killed = true;
377
483
  }
484
+
378
485
  try { unlinkSync(lockFile); } catch { /* non-fatal */ }
379
- return stalePid !== null;
486
+ return killed;
380
487
  }
381
488
 
382
489
  // Stop-and-restart helper for the stale-daemon branch (section 3a-pre). The
@@ -720,7 +827,7 @@ try {
720
827
 
721
828
  // ── Sync .claude/agents/ + .claude/skills/ recursively (#948) ──────
722
829
  // Pre-#948, agents and skills weren't manifest-tracked at all, so any
723
- // file moflo retired (e.g. the 49 ruflo-aspirational agents in #932 or
830
+ // file moflo retired (e.g. the 49 aspirational agents in #932 or
724
831
  // skill-builder in #945) would linger forever in consumer projects —
725
832
  // Claude Code kept loading them on every prompt, paying the per-prompt
726
833
  // roster tokens we just spent #932 fixing. Walking these dirs through
@@ -9,11 +9,12 @@ import os from 'os';
9
9
  import { getDaemonLockHolder } from '../services/daemon-lock.js';
10
10
  import { legacyMemoryDbPath, memoryDbCandidatePaths, memoryDbPath, } from '../services/moflo-paths.js';
11
11
  import { probeDbIntegrity } from '../services/memory-db-integrity-repair.js';
12
+ import { findProjectRoot } from '../services/project-root.js';
12
13
  import { errorDetail } from '../shared/utils/error-detail.js';
13
14
  export async function checkConfigFile() {
14
15
  // JSON configs (parse-validated). LEGACY-CONFIG: `.claude-flow.json` and
15
16
  // `claude-flow.config.json` filenames are still recognised so consumers
16
- // upgrading from pre-#699 moflo builds (upstream Ruflo) keep working
17
+ // upgrading from pre-#699 moflo builds keep working
17
18
  // without manual rename. Drift guard exempts these via LEGACY-CONFIG marker.
18
19
  const jsonPaths = [
19
20
  '.moflo/config.json',
@@ -195,9 +196,9 @@ export async function checkMemoryDbIntegrity(cwd = process.cwd()) {
195
196
  * Standard MCP-config search paths: home (Claude Desktop on macOS/Linux),
196
197
  * XDG config dir, project-local `.mcp.json`, and APPDATA on Windows.
197
198
  *
198
- * Shared by `checkMcpServers` (which inspects the FIRST config it finds and
199
- * reports on flo presence) and `checkDaemonWriteRouting` (which COUNTS
200
- * servers across all paths to detect the multi-process-clobber hazard).
199
+ * Shared by `checkMcpServers` (which inspects configs and reports on moflo
200
+ * presence) and `checkDaemonWriteRouting` (which COUNTS servers across all
201
+ * paths to detect the multi-process-clobber hazard).
201
202
  */
202
203
  function mcpConfigSearchPaths(cwd) {
203
204
  return [
@@ -226,25 +227,102 @@ function countMcpServers(cwd) {
226
227
  }
227
228
  return total;
228
229
  }
229
- export async function checkMcpServers() {
230
- for (const configPath of mcpConfigSearchPaths(process.cwd())) {
231
- if (existsSync(configPath)) {
232
- try {
233
- const content = JSON.parse(readFileSync(configPath, 'utf8'));
234
- const servers = content.mcpServers || content.servers || {};
235
- const count = Object.keys(servers).length;
236
- const hasClaudeFlow = 'moflo' in servers || 'claude-flow' in servers || 'claude-flow_alpha' in servers || 'ruflo' in servers || 'ruflo_alpha' in servers;
237
- if (hasClaudeFlow) {
238
- return { name: 'MCP Servers', status: 'pass', message: `${count} servers (flo configured)` };
239
- }
240
- return { name: 'MCP Servers', status: 'warn', message: `${count} servers (flo not found)`, fix: 'claude mcp add ruflo -- npx -y ruflo@latest mcp start' };
241
- }
242
- catch {
243
- // continue to next path
244
- }
230
+ /**
231
+ * Inspect the project's `.mcp.json` (the file `flo init` writes and Claude
232
+ * Code reads at the project level) and report whether moflo is present,
233
+ * absent, or the file is malformed.
234
+ *
235
+ * Scope: deliberately project-only. We previously also scanned
236
+ * `~/.claude/claude_desktop_config.json` and `%APPDATA%/Claude/...` — Claude
237
+ * **Desktop** paths which is a separate Anthropic product that doesn't
238
+ * host moflo's MCP server. The old wide scan caused #1126: a Claude
239
+ * Desktop preferences-only APPDATA file outranked a malformed project
240
+ * `.mcp.json`, surfacing "0 servers (flo not found)" while the actually
241
+ * broken project file went unrepaired. Claude Code stores user-level MCP
242
+ * registrations under `~/.claude.json` `projects[<path>].mcpServers`, but
243
+ * that's Claude Code internal state we don't author or rewrite; the
244
+ * project file is the canonical surface moflo owns end-to-end.
245
+ *
246
+ * Multi-writer / cross-ecosystem MCP-process counting still uses the wider
247
+ * `mcpConfigSearchPaths` via `countMcpServers` in
248
+ * `checkDaemonWriteRouting` — that check has a legitimate reason to see
249
+ * Claude Desktop processes.
250
+ *
251
+ * Exported so the auto-fixer (doctor-fixes.ts) can detect a malformed
252
+ * `.mcp.json` and regenerate it. Project root resolves via
253
+ * `findProjectRoot()` so a consumer running `flo healer` from a
254
+ * subdirectory still discovers the project file.
255
+ */
256
+ export function inspectMcpConfigs(cwd = process.cwd()) {
257
+ // Callers wanting project-root resolution pass it in (see `checkMcpServers`
258
+ // below). Defaulting to `process.cwd()` matches the established pattern in
259
+ // sibling checks (`checkMemoryDbIntegrity`, `countMcpServers`) and avoids a
260
+ // redundant FS walk on every doctor pass.
261
+ const configPath = join(cwd, '.mcp.json');
262
+ // Single read with the existence/parse branches handled by the catch —
263
+ // dropping the pre-`existsSync` guard closes a TOCTOU window where the
264
+ // file is deleted between the check and the read.
265
+ let content;
266
+ try {
267
+ content = JSON.parse(readFileSync(configPath, 'utf8'));
268
+ }
269
+ catch (e) {
270
+ if (e.code === 'ENOENT') {
271
+ return { status: 'not_found' };
245
272
  }
273
+ return { status: 'malformed', path: configPath, parseError: errorDetail(e) };
274
+ }
275
+ const serversValue = content.mcpServers ?? content.servers;
276
+ const servers = (serversValue && typeof serversValue === 'object')
277
+ ? serversValue
278
+ : {};
279
+ const count = Object.keys(servers).length;
280
+ const hasMoflo = 'moflo' in servers || 'claude-flow' in servers || 'claude-flow_alpha' in servers;
281
+ if (hasMoflo) {
282
+ return { status: 'valid_with_moflo', path: configPath, count };
283
+ }
284
+ return { status: 'valid_no_moflo', path: configPath, count };
285
+ }
286
+ export async function checkMcpServers(cwd = findProjectRoot()) {
287
+ const result = inspectMcpConfigs(cwd);
288
+ switch (result.status) {
289
+ case 'valid_with_moflo':
290
+ return { name: 'MCP Servers', status: 'pass', message: `${result.count} servers (moflo configured)` };
291
+ case 'valid_no_moflo':
292
+ return {
293
+ name: 'MCP Servers',
294
+ status: 'warn',
295
+ message: `${result.count} servers (moflo not registered)`,
296
+ fix: 'claude mcp add moflo -- npx -y moflo mcp start',
297
+ };
298
+ case 'malformed':
299
+ // #1126: distinguish "config malformed" from "moflo missing". Previously
300
+ // the loop silently caught the JSON.parse error and fell through,
301
+ // reporting "0 servers (flo not found)" — which led users to run
302
+ // `claude mcp add` against a still-broken file that would never parse.
303
+ // Now we surface the parse error and direct them at the auto-fixer
304
+ // that regenerates the file from `generateMCPJson`.
305
+ return {
306
+ name: 'MCP Servers',
307
+ status: 'warn',
308
+ message: `malformed JSON at ${result.path}: ${result.parseError}`,
309
+ fix: 'flo healer --fix -c mcp-servers',
310
+ };
311
+ case 'not_found':
312
+ return {
313
+ name: 'MCP Servers',
314
+ status: 'warn',
315
+ message: 'No MCP config found',
316
+ fix: 'claude mcp add moflo -- npx -y moflo mcp start',
317
+ };
246
318
  }
247
- return { name: 'MCP Servers', status: 'warn', message: 'No MCP config found', fix: 'claude mcp add moflo npx moflo mcp start' };
319
+ // Compile-time exhaustiveness guard: if a future status variant is added to
320
+ // `inspectMcpConfigs`'s return type without a matching case above, this
321
+ // assignment fails to compile (`string` is not assignable to `never`),
322
+ // forcing a corresponding update here. Better than a silent `default:`
323
+ // branch swallowing the new case as if it were `not_found`.
324
+ const _exhaustive = result.status;
325
+ return _exhaustive;
248
326
  }
249
327
  // Catches three failure modes (#895):
250
328
  // 1. File missing — session-start should have created it; warn user that
@@ -9,9 +9,12 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from '
9
9
  import { join } from 'path';
10
10
  import { output } from '../output.js';
11
11
  import { errorDetail } from '../shared/utils/error-detail.js';
12
+ import { atomicWriteFileSync } from '../shared/utils/atomic-file-write.js';
12
13
  import { repairHookWiring } from '../services/hook-wiring.js';
13
14
  import { getDaemonLockHolder } from '../services/daemon-lock.js';
15
+ import { findProjectRoot } from '../services/project-root.js';
14
16
  import { findZombieProcesses } from './doctor-zombies.js';
17
+ import { inspectMcpConfigs } from './doctor-checks-config.js';
15
18
  import { installClaudeCode, runCommand } from './doctor-checks-runtime.js';
16
19
  /** Run a shell command as a fix action. Returns true on exit code 0. */
17
20
  async function runFixCommand(cmd) {
@@ -200,6 +203,46 @@ export async function autoFixCheck(check) {
200
203
  return runFixCommand('npx moflo embeddings init --force');
201
204
  },
202
205
  'MCP Servers': async () => {
206
+ // #1126: distinguish "malformed JSON" from "moflo missing from valid
207
+ // config". The previous fix always ran `claude mcp add` — a no-op when
208
+ // the project-local `.mcp.json` was unparseable, because the command
209
+ // doesn't touch malformed project files.
210
+ const projectRoot = findProjectRoot();
211
+ const inspection = inspectMcpConfigs(projectRoot);
212
+ if (inspection.status === 'malformed' && inspection.path) {
213
+ try {
214
+ // Filesystem-safe timestamp: Date.now() is a digit-only integer so
215
+ // no `:` escape needed (per dogfooding.md § 6 cross-platform primitives).
216
+ const backupPath = `${inspection.path}.malformed-${Date.now()}`;
217
+ // The backup is a brand-new file at a unique timestamped path with
218
+ // no concurrent readers — a plain writeFileSync is enough; the
219
+ // atomic ceremony is only worth its cost when replacing a file a
220
+ // running process might re-open mid-write.
221
+ writeFileSync(backupPath, readFileSync(inspection.path, 'utf8'), 'utf-8');
222
+ const { generateMCPJson } = await import('../init/mcp-generator.js');
223
+ const { DEFAULT_INIT_OPTIONS } = await import('../init/types.js');
224
+ const regenerated = generateMCPJson({ ...DEFAULT_INIT_OPTIONS, targetDir: projectRoot });
225
+ // Confirm the regenerated content parses before clobbering the
226
+ // (broken) original — refuse to repair if our own generator emits
227
+ // unparseable JSON for any reason (defense in depth against a future
228
+ // generator regression silently emitting bad escapes again).
229
+ JSON.parse(regenerated);
230
+ // Atomic swap so a concurrent reader (e.g. Claude Code re-scanning
231
+ // .mcp.json during the fix) never sees a truncated file. The
232
+ // Windows-AV-lock verify window inside atomicWriteFileSync (#1015)
233
+ // gates the rename until the new bytes are readable.
234
+ atomicWriteFileSync(inspection.path, regenerated);
235
+ output.writeln(output.dim(` Regenerated ${inspection.path}; backup at ${backupPath}.`));
236
+ return true;
237
+ }
238
+ catch (e) {
239
+ output.writeln(output.warning(` Regeneration failed: ${errorDetail(e)}`));
240
+ return false;
241
+ }
242
+ }
243
+ // Valid config exists but moflo isn't registered — fall back to the
244
+ // claude-cli flow. This is the original behavior for the
245
+ // valid_no_moflo / not_found states.
203
246
  return runFixCommand('claude mcp add moflo -- npx -y moflo mcp start');
204
247
  },
205
248
  'Claude Code CLI': async () => {
@@ -27,7 +27,7 @@ function readCurrentVersion() {
27
27
  const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
28
28
  if (pkg.version &&
29
29
  typeof pkg.name === 'string' &&
30
- (pkg.name === 'moflo' || pkg.name === 'claude-flow' || pkg.name === 'ruflo')) {
30
+ (pkg.name === 'moflo' || pkg.name === 'claude-flow')) {
31
31
  return pkg.version;
32
32
  }
33
33
  }
@@ -62,8 +62,6 @@ const commandLoaders = {
62
62
  benchmark: () => import('./benchmark.js'),
63
63
  // Guidance Control Plane
64
64
  guidance: () => import('./guidance.js'),
65
- // RVFA Appliance Management
66
- appliance: () => import('./appliance.js'),
67
65
  // MoFlo Spell Gates
68
66
  gate: () => import('./gate.js'),
69
67
  // Feature Orchestrator
@@ -137,7 +135,6 @@ import { issuesCommand } from './issues.js';
137
135
  import updateCommand from './update.js';
138
136
  import { processCommand } from './process.js';
139
137
  import { guidanceCommand } from './guidance.js';
140
- import { applianceCommand } from './appliance.js';
141
138
  import { diagnoseCommand } from './diagnose.js';
142
139
  import { githubCommand } from './github.js';
143
140
  // Pre-populate cache with core commands
@@ -184,7 +181,6 @@ export { performanceCommand } from './performance.js';
184
181
  export { securityCommand } from './security.js';
185
182
  export { hiveMindCommand } from './hive-mind.js';
186
183
  export { guidanceCommand } from './guidance.js';
187
- export { applianceCommand } from './appliance.js';
188
184
  export { diagnoseCommand } from './diagnose.js';
189
185
  export { githubCommand } from './github.js';
190
186
  // Lazy-loaded command re-exports (for backwards compatibility, but async-only)
@@ -209,7 +205,6 @@ export async function getRouteCommand() { return loadCommand('route'); }
209
205
  export async function getProgressCommand() { return loadCommand('progress'); }
210
206
  export async function getIssuesCommand() { return loadCommand('issues'); }
211
207
  export async function getGuidanceCommand() { return loadCommand('guidance'); }
212
- export async function getApplianceCommand() { return loadCommand('appliance'); }
213
208
  /**
214
209
  * Core commands loaded synchronously (available immediately)
215
210
  * Advanced commands loaded on-demand for faster startup
@@ -285,7 +280,6 @@ export const commandsByCategory = {
285
280
  issuesCommand,
286
281
  updateCommand,
287
282
  processCommand,
288
- applianceCommand,
289
283
  githubCommand,
290
284
  ],
291
285
  };
@@ -70,8 +70,8 @@ const COMMANDS_MAP = {};
70
70
  * Agents to copy based on configuration. Exported for integrity tests.
71
71
  *
72
72
  * Each value is a directory name under `.claude/agents/` that ships in the
73
- * moflo package. After #932 retired ~50 ruflo-aspirational agents, the set
74
- * is narrowed to actual development specialties Claude is likely to invoke.
73
+ * moflo package. After #932 retired ~50 aspirational agents, the set is
74
+ * narrowed to actual development specialties Claude is likely to invoke.
75
75
  */
76
76
  export const AGENTS_MAP = {
77
77
  core: ['core'],
@@ -12,7 +12,7 @@ import { getSwarmCoordinator, isSwarmCoordinatorInitialized, } from './swarm-coo
12
12
  import { scaleHandler, SCALE_STRATEGIES, TARGET_AGENTS_MIN, TARGET_AGENTS_MAX, } from './swarm-scale-handler.js';
13
13
  import { findProjectRoot } from '../services/project-root.js';
14
14
  import { MOFLO_DIR } from '../services/moflo-paths.js';
15
- // Inputs accepted by the MCP layer (covers Ruflo aliases). The coordinator's
15
+ // Inputs accepted by the MCP layer (covers legacy aliases). The coordinator's
16
16
  // TopologyType is narrower: 'mesh' | 'hierarchical' | 'centralized' | 'hybrid'.
17
17
  const TOPOLOGY_MAP = {
18
18
  hierarchical: 'hierarchical',
@@ -23,9 +23,8 @@ const TOPOLOGY_MAP = {
23
23
  'hierarchical-mesh': 'hybrid',
24
24
  hybrid: 'hybrid',
25
25
  };
26
- // Ported from Ruflo v3/mcp/tools/swarm-tools.ts. `unanimous`/`weighted`/
27
- // `majority` are the user-facing aliases; the coordinator only speaks
28
- // `byzantine`/`raft`/`gossip`/`paxos`.
26
+ // `unanimous`/`weighted`/`majority` are the user-facing aliases; the
27
+ // coordinator only speaks `byzantine`/`raft`/`gossip`/`paxos`.
29
28
  const CONSENSUS_MAP = {
30
29
  unanimous: { algorithm: 'byzantine', threshold: 1.0 },
31
30
  byzantine: { algorithm: 'byzantine', threshold: 1.0 },