moflo 4.10.7 → 4.10.8

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 (32) hide show
  1. package/.claude/guidance/shipped/moflo-cli-reference.md +1 -1
  2. package/.claude/guidance/shipped/moflo-memory-strategy.md +1 -1
  3. package/.claude/guidance/shipped/moflo-yaml-reference.md +4 -4
  4. package/.claude/skills/memory-optimization/SKILL.md +1 -1
  5. package/.claude/skills/memory-patterns/SKILL.md +3 -3
  6. package/.claude/skills/vector-search/SKILL.md +2 -2
  7. package/README.md +5 -5
  8. package/bin/lib/daemon-port.mjs +66 -0
  9. package/dist/src/cli/commands/daemon.js +31 -10
  10. package/dist/src/cli/commands/doctor-checks-config.js +139 -1
  11. package/dist/src/cli/commands/doctor-fixes.js +75 -2
  12. package/dist/src/cli/commands/doctor-registry.js +10 -1
  13. package/dist/src/cli/commands/memory.js +8 -8
  14. package/dist/src/cli/commands/neural.js +8 -6
  15. package/dist/src/cli/config/moflo-config.js +68 -3
  16. package/dist/src/cli/index.js +18 -19
  17. package/dist/src/cli/init/moflo-yaml-template.js +1 -1
  18. package/dist/src/cli/mcp-server.js +59 -10
  19. package/dist/src/cli/mcp-tools/memory-tools.js +46 -27
  20. package/dist/src/cli/memory/auto-memory-bridge.js +1 -1
  21. package/dist/src/cli/memory/controllers/attestation-log.js +1 -1
  22. package/dist/src/cli/memory/controllers/causal-graph.js +1 -1
  23. package/dist/src/cli/memory/daemon-write-client.js +178 -49
  24. package/dist/src/cli/memory/database-provider.js +58 -3
  25. package/dist/src/cli/memory/intelligence.js +54 -26
  26. package/dist/src/cli/memory/memory-initializer.js +21 -11
  27. package/dist/src/cli/services/daemon-dashboard.js +94 -25
  28. package/dist/src/cli/services/daemon-lock.js +390 -3
  29. package/dist/src/cli/services/daemon-port.js +217 -0
  30. package/dist/src/cli/version.js +1 -1
  31. package/package.json +2 -2
  32. package/dist/src/cli/config-adapter.js +0 -182
@@ -13,7 +13,7 @@
13
13
  | `init` | 4 | Project initialization with wizard, presets, skills, hooks |
14
14
  | `agent` | 8 | Agent lifecycle (spawn, list, status, stop, metrics, pool, health, logs) |
15
15
  | `swarm` | 6 | Multi-agent swarm coordination and orchestration |
16
- | `memory` | 11 | sql.js + HNSW vector search, 150x-12,500x faster |
16
+ | `memory` | 11 | node:sqlite + HNSW vector search, 150x-12,500x faster |
17
17
  | `mcp` | 9 | MCP server management and tool execution |
18
18
  | `task` | 6 | Task creation, assignment, and lifecycle |
19
19
  | `session` | 7 | Session state management and persistence |
@@ -9,7 +9,7 @@
9
9
  Source files (`.claude/guidance/*.md`, `docs/**/*.md`, code, tests) flow through four stages:
10
10
 
11
11
  1. **Index** — `bin/index-*.mjs` chunks markdown on `##` headers and walks code/tests for structural facts
12
- 2. **Store** — entries land in `.moflo/moflo.db` (SQLite via sql.js) with metadata + RAG links
12
+ 2. **Store** — entries land in `.moflo/moflo.db` (SQLite via Node 22's built-in `node:sqlite`) with metadata + RAG links
13
13
  3. **Embed** — `bin/build-embeddings.mjs` generates 384-dim vectors via `fastembed` (mandatory, see ADR-EMB-001)
14
14
  4. **Search** — `mcp__moflo__memory_search` (preferred), `npx flo memory search` (fallback), or `npx flo-search` (verbose) hit an HNSW index for nearest-neighbor lookup
15
15
 
@@ -47,7 +47,7 @@ auto_index:
47
47
 
48
48
  # Memory backend
49
49
  memory:
50
- backend: sql.js # sql.js (WASM) | json
50
+ backend: node-sqlite # node-sqlite (default) | rvf (pure-TS fallback) | json (last resort). Passed to createDatabase() as the preferred provider (#1144).
51
51
  embedding_model: Xenova/all-MiniLM-L6-v2 # 384-dim neural embeddings
52
52
  namespace: default # Default namespace for memory operations
53
53
 
@@ -156,9 +156,9 @@ CLAUDE_FLOW_LOG_LEVEL=info # debug | info | warn | error
156
156
  # MCP Server (stdio transport — no port)
157
157
  CLAUDE_FLOW_MCP_TRANSPORT=stdio
158
158
 
159
- # Memory backend
160
- CLAUDE_FLOW_MEMORY_BACKEND=hybrid # hybrid | sqlite | agentdb (legacy)
161
- CLAUDE_FLOW_MEMORY_TYPE=sqlite # storage type override
159
+ # Memory backend (legacy SystemConfig env vars — moflo.yaml `memory.backend` is the modern surface)
160
+ CLAUDE_FLOW_MEMORY_BACKEND=sqlite # informational only; not consumed by selectProvider
161
+ CLAUDE_FLOW_MEMORY_TYPE=sqlite # SystemConfig override (legacy)
162
162
  ```
163
163
 
164
164
  Variable names retain the `CLAUDE_FLOW_` prefix for backward compatibility with consumers upgraded from claude-flow.
@@ -112,7 +112,7 @@ const stats = await mcp.memory_stats({});
112
112
  - **Don't rebuild the index on every test.** Use a module-level singleton + `beforeAll`. HNSW cold-boot is ~5s.
113
113
  - **Don't raise `ef` globally.** Raise it on the specific queries that need recall. Default is fine for 90% of calls.
114
114
  - **Don't quantize a small corpus.** Below ~500k vectors the RAM saving doesn't justify the recall cost.
115
- - **Don't measure in dev mode.** sql.js WASM behaves differently under `NODE_ENV=production`; benches should match the target.
115
+ - **Don't measure in dev mode.** The memory stack behaves differently under `NODE_ENV=production`; benches should match the target.
116
116
 
117
117
  ## See Also
118
118
 
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: "memory-patterns"
3
- description: "Persistent memory patterns for moflo agents — session memory, long-term knowledge, pattern learning, and cross-session context via moflo's sql.js + HNSW vector store. Use when building stateful agents or assistants that need to remember across runs."
3
+ description: "Persistent memory patterns for moflo agents — session memory, long-term knowledge, pattern learning, and cross-session context via moflo's node:sqlite + HNSW vector store. Use when building stateful agents or assistants that need to remember across runs."
4
4
  ---
5
5
 
6
6
  # MoFlo Memory Patterns
7
7
 
8
- Persistent, semantically-searchable memory for moflo-enabled projects. Backed by `.swarm/memory.db` (sql.js + HNSW vector index) and exposed through MCP tools.
8
+ Persistent, semantically-searchable memory for moflo-enabled projects. Backed by `.moflo/moflo.db` (node:sqlite + HNSW vector index) and exposed through MCP tools.
9
9
 
10
10
  ## Core API
11
11
 
@@ -124,7 +124,7 @@ This is the same fan-out the `/flo` spell does — cheap (HNSW, parallel) and re
124
124
 
125
125
  ## Persistence & Indexing
126
126
 
127
- - File: `.swarm/memory.db` at project root (sql.js).
127
+ - File: `.moflo/moflo.db` at project root (node:sqlite, Node 22+ built-in).
128
128
  - Embeddings: built by cli's embeddings module; indexed with HNSW from `src/cli/memory/`.
129
129
  - Cold-start cost: ~5 seconds to initialize HNSW. Tests should share a single instance (`beforeAll`, not `beforeEach`).
130
130
  - Namespace isolation: each namespace is a logical partition, but the HNSW index spans the table. Query time scales with `limit` and `threshold`, not total row count.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: "vector-search"
3
- description: "Semantic vector search with moflo — RAG over your own documents, similarity matching, context-aware retrieval via HNSW (sql.js-backed). Use when building retrieval layers for chat, search, or context-assembly."
3
+ description: "Semantic vector search with moflo — RAG over your own documents, similarity matching, context-aware retrieval via HNSW (node:sqlite-backed). Use when building retrieval layers for chat, search, or context-assembly."
4
4
  ---
5
5
 
6
6
  # MoFlo Vector Search (RAG)
7
7
 
8
- Semantic search over your own documents, backed by moflo's HNSW index in `.swarm/memory.db`. Small enough to ship in a devDependency; fast enough for interactive retrieval at 100k–1M vectors.
8
+ Semantic search over your own documents, backed by moflo's HNSW index in `.moflo/moflo.db` (node:sqlite, Node 22+ built-in). Small enough to ship in a devDependency; fast enough for interactive retrieval at 100k–1M vectors.
9
9
 
10
10
  ## When to Use This vs `memory-patterns`
11
11
 
package/README.md CHANGED
@@ -30,7 +30,7 @@ MoFlo makes deliberate choices so you don't have to:
30
30
  - **Fully self-contained** — No external services, no cloud dependencies, no API keys. Everything runs locally on your machine.
31
31
  - **Minimal dependencies** — small runtime dep set, all WASM or prebuilt binaries. No native compilation, no `node-gyp`, no platform-specific build steps.
32
32
  - **Node.js runtime** — Targets Node.js specifically. All scripts, hooks, and tooling are JavaScript/TypeScript. No Python, no Rust binaries, no native compilation.
33
- - **sql.js (WASM)** — The memory database uses sql.js, a pure WebAssembly build of SQLite. No native `better-sqlite3` bindings to compile, no platform-specific build steps. Works identically on Windows, macOS, and Linux.
33
+ - **node:sqlite (built-in)** — The memory database uses Node 22's built-in SQLite engine. No `better-sqlite3` native bindings to compile, no WASM round-trip, no platform-specific build steps. Works identically on Windows, macOS, and Linux.
34
34
  - **Neural embeddings by default** — 384-dimensional embeddings using `all-MiniLM-L6-v2`. No hash fallback, no peer-optional setup, no install prompts — real semantic search works out of the box. A `postinstall` step trims the embedding runtime to your platform and strips GPU-only libraries the runtime never loads, reclaiming roughly 340 MB on Linux and 150 MB on Windows from a fresh install. Set `MOFLO_NO_PRUNE=1` to skip the trim, or `ONNXRUNTIME_NODE_INSTALL_CUDA=true` to keep CUDA GPU support.
35
35
  - **Full learning stack wired up OOTB** — All configured and functional from `flo init`, no manual setup:
36
36
  - **SONA** (Self-Optimizing Neural Architecture) — learns from task trajectories
@@ -537,7 +537,7 @@ flo diagnose --json # JSON output for CI/automation
537
537
  | **MCP Spell Integration** | Bridge between MCP tools and spell engine functions correctly |
538
538
  | **Hook Execution** | Hook executor is functional and can fire hooks |
539
539
  | **Gate Health** | All gate cases, hook bindings, and state file are intact |
540
- | **MofloDb Bridge** | Memory DB adapter (sql.js + HNSW) is wired and routable |
540
+ | **MofloDb Bridge** | Memory DB adapter (node:sqlite + HNSW) is wired and routable |
541
541
  | **Sandbox Tier** | Detects which sandbox backend is available (Docker / bwrap / sandbox-exec / none) |
542
542
 
543
543
  **Auto-fix mode** (`flo healer --fix`) attempts to repair each failing check automatically:
@@ -645,7 +645,7 @@ These are the backend systems that hooks and commands interact with.
645
645
 
646
646
  | System | What It Does | Why It Matters | Enabled OOTB |
647
647
  |--------|-------------|----------------|:---:|
648
- | **Semantic Memory** | SQLite database (sql.js/WASM) storing knowledge entries with 384-dim vector embeddings | Your AI assistant accumulates project knowledge across sessions instead of starting from scratch each time | Yes |
648
+ | **Semantic Memory** | SQLite database (Node's built-in `node:sqlite`) storing knowledge entries with 384-dim vector embeddings | Your AI assistant accumulates project knowledge across sessions instead of starting from scratch each time | Yes |
649
649
  | **HNSW Vector Search** | Hierarchical Navigable Small World index for fast nearest-neighbor search | Searches across thousands of stored entries return in milliseconds instead of scanning linearly | Yes |
650
650
  | **Guidance Indexing** | Chunks markdown docs into overlapping segments, embeds each with MiniLM-L6-v2 | Your project documentation becomes searchable by meaning ("how does auth work?") not just keywords | Yes |
651
651
  | **Code Map** | Parses source files for exports, classes, functions, types | The AI can answer "where is X defined?" from the index instead of running Glob/Grep | Yes |
@@ -720,7 +720,7 @@ Routing outcomes persist across sessions. You can inspect them with `flo hooks p
720
720
 
721
721
  ### Memory & Knowledge Storage
722
722
 
723
- MoFlo uses a SQLite database (via sql.js/WASM — no native deps) to store three types of knowledge:
723
+ MoFlo uses a SQLite database (via Node 22's built-in `node:sqlite` — no native deps) to store three types of knowledge:
724
724
 
725
725
  | Namespace | What's Stored | How It Gets There |
726
726
  |-----------|---------------|-------------------|
@@ -861,7 +861,7 @@ MoFlo started from [Ruflo/Claude Flow](https://github.com/ruvnet/ruflo) but is n
861
861
 
862
862
  My use case was just one of those many scenarios: day-to-day local coding, enhancing my normal Claude Code experience on a single project. The original supported this — it was all in there — but because the project served so many different needs, I found myself configuring and tailoring things for my specific setup each time I pulled in updates. That isn't a shortcoming of the original; it's the natural trade-off of a tool designed to be that flexible and powerful.
863
863
 
864
- So I started from that foundation and narrowed the focus to my particular corner of it. I baked in the defaults I kept setting manually, added automatic indexing and memory gating at session start, and tuned the out-of-box experience so that `npm install` and `flo init` gets you straight to coding. Over time MoFlo grew its own architecture (workspace collapse, in-tree fastembed runtime, sql.js + HNSW memory layer, spell engine, daemon-driven scheduling) and the two projects fully diverged.
864
+ So I started from that foundation and narrowed the focus to my particular corner of it. I baked in the defaults I kept setting manually, added automatic indexing and memory gating at session start, and tuned the out-of-box experience so that `npm install` and `flo init` gets you straight to coding. Over time MoFlo grew its own architecture (workspace collapse, in-tree fastembed runtime, node:sqlite + HNSW memory layer, spell engine, daemon-driven scheduling) and the two projects fully diverged.
865
865
 
866
866
  If you're exploring the full breadth of agent orchestration, go look at [Ruflo/Claude Flow](https://github.com/ruvnet/ruflo) — it's the real deal. If your needs are similar to mine — a focused, opinionated local dev setup that just works — MoFlo is for you.
867
867
 
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Pure-JS counterpart to src/cli/services/daemon-port.ts.
3
+ *
4
+ * Lives in bin/lib because session-start-launcher.mjs and other bin/ scripts
5
+ * run before any TS compilation has happened. The TS file is the canonical
6
+ * API; this file MUST stay algorithmically identical (asserted by
7
+ * `tests/system/daemon-port-twin.test.ts`).
8
+ *
9
+ * See `src/cli/services/daemon-port.ts` for the full doc + history.
10
+ */
11
+ import { createHash } from 'node:crypto';
12
+ import { existsSync, readFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+
15
+ export const PORT_RANGE_BASE = 33000;
16
+ export const PORT_RANGE_SIZE = 1000;
17
+ export const LEGACY_DEFAULT_PORT = 3117;
18
+
19
+ export function readEnvPortOverride() {
20
+ const raw = process.env.MOFLO_DAEMON_PORT;
21
+ if (!raw) return null;
22
+ const n = parseInt(raw, 10);
23
+ if (!Number.isFinite(n) || n < 1 || n > 65535) return null;
24
+ return n;
25
+ }
26
+
27
+ export function resolveProjectPort(projectRoot) {
28
+ const envPort = readEnvPortOverride();
29
+ if (envPort != null) return envPort;
30
+ const hash = createHash('sha256').update(projectRoot).digest();
31
+ return PORT_RANGE_BASE + (hash.readUInt16BE(0) % PORT_RANGE_SIZE);
32
+ }
33
+
34
+ export function resolveClientPort(projectRoot) {
35
+ const envPort = readEnvPortOverride();
36
+ if (envPort != null) return envPort;
37
+
38
+ try {
39
+ const lockFile = join(projectRoot, '.moflo', 'daemon.lock');
40
+ if (existsSync(lockFile)) {
41
+ const lock = JSON.parse(readFileSync(lockFile, 'utf-8'));
42
+ const lockPort = typeof lock?.port === 'number' ? lock.port : null;
43
+ if (lockPort && Number.isFinite(lockPort) && lockPort > 0 && lockPort < 65536) {
44
+ return lockPort;
45
+ }
46
+ }
47
+ } catch {
48
+ // fall through
49
+ }
50
+
51
+ return resolveProjectPort(projectRoot);
52
+ }
53
+
54
+ export function serverPortCandidates(projectRoot, maxAttempts = 10) {
55
+ const envPort = readEnvPortOverride();
56
+ if (envPort != null) return [envPort];
57
+
58
+ const base = resolveProjectPort(projectRoot);
59
+ const attempts = Math.min(Math.max(1, maxAttempts), PORT_RANGE_SIZE);
60
+ const ports = [];
61
+ for (let i = 0; i < attempts; i++) {
62
+ const candidate = PORT_RANGE_BASE + ((base - PORT_RANGE_BASE + i) % PORT_RANGE_SIZE);
63
+ ports.push(candidate);
64
+ }
65
+ return ports;
66
+ }
@@ -32,13 +32,13 @@ import { errorDetail } from '../shared/utils/error-detail.js';
32
32
  export function resolveDashboardPort(flagValue, envValue) {
33
33
  const source = flagValue ?? envValue;
34
34
  if (!source)
35
- return { ok: true, port: DEFAULT_DASHBOARD_PORT };
35
+ return { ok: true, port: DEFAULT_DASHBOARD_PORT, explicit: false };
36
36
  const parsed = parseInt(source, 10);
37
37
  if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
38
38
  const label = flagValue ? 'dashboard port' : 'MOFLO_DAEMON_PORT';
39
39
  return { ok: false, error: `Invalid ${label}: ${source} (must be 1-65535)` };
40
40
  }
41
- return { ok: true, port: parsed };
41
+ return { ok: true, port: parsed, explicit: true };
42
42
  }
43
43
  // Start daemon subcommand
44
44
  const startCommand = {
@@ -76,6 +76,7 @@ const startCommand = {
76
76
  return { success: false, exitCode: 1 };
77
77
  }
78
78
  const dashboardPort = portResult.port;
79
+ const dashboardPortExplicit = portResult.explicit;
79
80
  // Parse resource threshold overrides from CLI flags
80
81
  const config = {};
81
82
  const rawMaxCpu = ctx.flags.maxCpuLoad;
@@ -109,7 +110,7 @@ const startCommand = {
109
110
  }
110
111
  // Background mode (default): fork a detached process
111
112
  if (!foreground) {
112
- return startBackgroundDaemon(projectRoot, quiet, rawMaxCpu, rawMinMem, dashboardPort, noDashboard);
113
+ return startBackgroundDaemon(projectRoot, quiet, rawMaxCpu, rawMinMem, dashboardPortExplicit ? dashboardPort : undefined, noDashboard);
113
114
  }
114
115
  // Foreground mode: run in current process (blocks terminal)
115
116
  try {
@@ -151,7 +152,7 @@ const startCommand = {
151
152
  const status = daemon.getStatus();
152
153
  spinner.succeed('Worker daemon started (foreground mode)');
153
154
  const { dashboard } = await attachDaemonServices(daemon, {
154
- projectRoot, noDashboard, dashboardPort, verbose: true,
155
+ projectRoot, noDashboard, dashboardPort, dashboardPortExplicit, verbose: true,
155
156
  });
156
157
  output.writeln();
157
158
  output.printBox([
@@ -207,7 +208,7 @@ const startCommand = {
207
208
  }
208
209
  else {
209
210
  const daemon = await startDaemon(projectRoot, config);
210
- await attachDaemonServices(daemon, { projectRoot, noDashboard, dashboardPort, verbose: false });
211
+ await attachDaemonServices(daemon, { projectRoot, noDashboard, dashboardPort, dashboardPortExplicit, verbose: false });
211
212
  await new Promise(() => { }); // Keep alive
212
213
  }
213
214
  return { success: true };
@@ -243,15 +244,25 @@ async function attachDaemonServices(daemon, opts) {
243
244
  if (!opts.noDashboard) {
244
245
  try {
245
246
  dashboard = await startDashboard(daemon, {
246
- port: opts.dashboardPort,
247
+ // Pass the resolved port only when the caller explicitly pinned it
248
+ // (CLI flag / env). Otherwise let startDashboard pick the
249
+ // deterministic per-project port via serverPortCandidates (#1145).
250
+ port: opts.dashboardPortExplicit ? opts.dashboardPort : undefined,
247
251
  memory,
248
252
  schedulerEnabledInConfig: schedulerConfig.enabled,
253
+ projectRoot: opts.projectRoot,
249
254
  });
250
255
  if (opts.verbose)
251
256
  output.printSuccess(`The Luminarium: http://localhost:${dashboard.port}`);
252
257
  }
253
258
  catch (err) {
254
- logWarn(`The Luminarium failed to start: ${errorDetail(err)}`);
259
+ // #1145 §9.4 hard-fail on bind exhaustion. Pre-#1145 we swallowed
260
+ // this; the daemon stayed alive doing worker-only work while clients
261
+ // routed to whichever daemon happened to be on the legacy default
262
+ // port. Re-throw so the spawn launcher sees a non-zero exit and the
263
+ // healer flags the failure instead of silently continuing.
264
+ logWarn(`The Luminarium failed to bind — daemon will exit so clients don't silently route to a foreign daemon: ${errorDetail(err)}`);
265
+ throw err;
255
266
  }
256
267
  }
257
268
  if (!schedulerConfig.enabled) {
@@ -350,11 +361,14 @@ async function startBackgroundDaemon(projectRoot, quiet, maxCpuLoad, minFreeMemo
350
361
  if (minFreeMemory && SPAWN_NUMERIC_RE.test(minFreeMemory)) {
351
362
  spawnArgs.push('--min-free-memory', minFreeMemory);
352
363
  }
353
- // Forward dashboard flags
364
+ // Forward dashboard flags. With #1145 the foreground child resolves its
365
+ // own deterministic per-project port when no `--dashboard-port` flag is
366
+ // passed — only forward the flag when the caller explicitly pinned it
367
+ // (signaled by `dashboardPort` being defined here).
354
368
  if (noDashboard) {
355
369
  spawnArgs.push('--no-dashboard');
356
370
  }
357
- else if (dashboardPort && dashboardPort !== DEFAULT_DASHBOARD_PORT) {
371
+ else if (dashboardPort != null) {
358
372
  spawnArgs.push('--dashboard-port', String(dashboardPort));
359
373
  }
360
374
  const daemonEnv = {
@@ -413,7 +427,14 @@ async function startBackgroundDaemon(projectRoot, quiet, maxCpuLoad, minFreeMemo
413
427
  if (!quiet) {
414
428
  output.printSuccess(`Daemon started in background (PID: ${pid})`);
415
429
  if (!noDashboard) {
416
- output.printInfo(`The Luminarium: http://localhost:${dashboardPort ?? DEFAULT_DASHBOARD_PORT}`);
430
+ if (dashboardPort != null) {
431
+ output.printInfo(`The Luminarium: http://localhost:${dashboardPort}`);
432
+ }
433
+ else {
434
+ // #1145 — port is project-deterministic and assigned at bind time;
435
+ // read it from .moflo/daemon.lock once the child finishes startup.
436
+ output.printInfo(`The Luminarium: see http://localhost:<port from .moflo/daemon.lock>`);
437
+ }
417
438
  }
418
439
  output.printInfo(`Logs: ${logFile}`);
419
440
  output.printInfo(`Stop with: claude-flow daemon stop`);
@@ -6,7 +6,8 @@
6
6
  import { existsSync, readFileSync, statSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import os from 'os';
9
- import { getDaemonLockHolder } from '../services/daemon-lock.js';
9
+ import { findProjectDaemonPids, getDaemonLockHolder, getDaemonLockPayload, } from '../services/daemon-lock.js';
10
+ import { resolveClientPort, LEGACY_DEFAULT_PORT, probeDaemonHealth as probeDaemonHealthIdentity, normalizeProjectRoot, } from '../services/daemon-port.js';
10
11
  import { legacyMemoryDbPath, memoryDbCandidatePaths, memoryDbPath, } from '../services/moflo-paths.js';
11
12
  import { probeDbIntegrity } from '../services/memory-db-integrity-repair.js';
12
13
  import { findProjectRoot } from '../services/project-root.js';
@@ -102,6 +103,143 @@ export async function checkDaemonStatus() {
102
103
  return { name: 'Daemon Status', status: 'warn', message: `Unable to check: ${errorDetail(e)}`, fix: 'claude-flow daemon status' };
103
104
  }
104
105
  }
106
+ /**
107
+ * Daemon identity check (#1145).
108
+ *
109
+ * Reads `.moflo/daemon.lock` → probes `/api/health` on the recorded port →
110
+ * confirms the daemon's reported `projectRoot` matches `findProjectRoot()`.
111
+ * Catches the silent cross-project routing class where two moflo daemons
112
+ * (e.g. moflo-dev + a consumer) share a port and the client hits the wrong
113
+ * one.
114
+ *
115
+ * Status semantics:
116
+ * - `pass` — no daemon (cleanly absent) OR identity matches.
117
+ * - `warn` — daemon has no port in lock (pre-#1145 daemon; harmless if
118
+ * no collision, but should be recycled to upgrade).
119
+ * - `fail` — `/api/health` reports a different project. Routing is
120
+ * polluted; the healer fix kills the foreign-identity daemon and
121
+ * respawns the local one.
122
+ */
123
+ export async function checkDaemonIdentity(cwd = process.cwd()) {
124
+ const projectRoot = findProjectRoot({ cwd });
125
+ const payload = getDaemonLockPayload(projectRoot);
126
+ if (!payload) {
127
+ return {
128
+ name: 'Daemon Identity Match',
129
+ status: 'pass',
130
+ message: 'No daemon running (nothing to verify)',
131
+ };
132
+ }
133
+ // Lock present but no port field — pre-#1145 daemon. Identity check is
134
+ // best-effort; surface as warn so the user knows to recycle but don't
135
+ // hard-fail unless we can prove cross-project routing.
136
+ const portFromLock = payload.port;
137
+ const probePort = portFromLock ?? resolveClientPort(projectRoot);
138
+ if (!portFromLock) {
139
+ // Try the legacy default explicitly so consumers running an
140
+ // un-upgraded daemon still get a useful diagnostic.
141
+ const probe = await probeDaemonHealthIdentity(LEGACY_DEFAULT_PORT, 1500);
142
+ if (probe.kind === 'identity'
143
+ && normalizeProjectRoot(probe.projectRoot) !== normalizeProjectRoot(projectRoot)) {
144
+ return {
145
+ name: 'Daemon Identity Match',
146
+ status: 'fail',
147
+ message: `Daemon at 127.0.0.1:${LEGACY_DEFAULT_PORT} claims project '${probe.projectRoot}' — cross-project routing active`,
148
+ fix: 'flo healer --fix -c daemon-identity',
149
+ };
150
+ }
151
+ return {
152
+ name: 'Daemon Identity Match',
153
+ status: 'warn',
154
+ message: 'Daemon lock has no port field — pre-#1145 daemon; recycle to enable port discovery',
155
+ fix: 'flo healer --fix -c daemon-identity',
156
+ };
157
+ }
158
+ const probe = await probeDaemonHealthIdentity(probePort, 1500);
159
+ if (probe.kind === 'unreachable') {
160
+ return {
161
+ name: 'Daemon Identity Match',
162
+ status: 'warn',
163
+ message: `Daemon at 127.0.0.1:${probePort} unreachable on /api/health`,
164
+ fix: 'flo healer --fix -c daemon-identity',
165
+ };
166
+ }
167
+ if (probe.kind === 'legacy') {
168
+ return {
169
+ name: 'Daemon Identity Match',
170
+ status: 'warn',
171
+ message: `Daemon at 127.0.0.1:${probePort} has no /api/health (legacy) — recycle to upgrade`,
172
+ fix: 'flo healer --fix -c daemon-identity',
173
+ };
174
+ }
175
+ if (normalizeProjectRoot(probe.projectRoot) !== normalizeProjectRoot(projectRoot)) {
176
+ return {
177
+ name: 'Daemon Identity Match',
178
+ status: 'fail',
179
+ message: `Daemon at 127.0.0.1:${probePort} claims project '${probe.projectRoot}' but cwd is '${projectRoot}'`,
180
+ fix: 'flo healer --fix -c daemon-identity',
181
+ };
182
+ }
183
+ return {
184
+ name: 'Daemon Identity Match',
185
+ status: 'pass',
186
+ message: `OK (port ${probePort}, pid ${payload.pid})`,
187
+ };
188
+ }
189
+ /**
190
+ * Same-project orphan daemon check (#1150).
191
+ *
192
+ * Counts moflo daemon processes whose command line is rooted at THIS
193
+ * project's CLI binary. Healthy state: 0 (no daemon) or 1 (the lock-holder).
194
+ * Failure state: >1 — multiple daemons are racing for the indexer lock and
195
+ * writing to the same `daemon-state.json`.
196
+ *
197
+ * Distinct from `Daemon Identity Match`, which catches a DIFFERENT project's
198
+ * daemon answering on a port. This check catches multiple SAME-project
199
+ * daemons sharing the project root but with one of them holding a stale or
200
+ * unlinked lock (the orphan path).
201
+ *
202
+ * Status semantics:
203
+ * - `pass` — 0 or 1 same-project daemon.
204
+ * - `fail` — 2+ same-project daemons. Auto-fix terminates all but the
205
+ * lock-recorded PID (or all + respawn if no lock matches a live one).
206
+ * The "no live lock-holder" sub-case stays `fail` rather than `warn`:
207
+ * a stale lock alongside live orphan daemons is a strictly worse state
208
+ * than an orphan that the lock knows about, not a softer one.
209
+ * - `warn` — the OS process scan itself failed (platform introspection
210
+ * unavailable). The healer is offered as a fallback but isn't binding.
211
+ */
212
+ export async function checkDaemonOrphan(cwd = process.cwd()) {
213
+ const projectRoot = findProjectRoot({ cwd });
214
+ let pids;
215
+ try {
216
+ pids = findProjectDaemonPids(projectRoot);
217
+ }
218
+ catch (e) {
219
+ return {
220
+ name: 'Daemon Orphan',
221
+ status: 'warn',
222
+ message: `Process scan failed: ${errorDetail(e)}`,
223
+ };
224
+ }
225
+ if (pids.length === 0) {
226
+ return { name: 'Daemon Orphan', status: 'pass', message: 'No daemon running (nothing to verify)' };
227
+ }
228
+ if (pids.length === 1) {
229
+ return { name: 'Daemon Orphan', status: 'pass', message: `1 daemon (pid ${pids[0]})` };
230
+ }
231
+ const lockHolder = getDaemonLockHolder(projectRoot);
232
+ const lockHolderInPids = lockHolder != null && pids.includes(lockHolder);
233
+ const orphanPids = lockHolderInPids ? pids.filter(p => p !== lockHolder) : pids;
234
+ return {
235
+ name: 'Daemon Orphan',
236
+ status: 'fail',
237
+ message: lockHolderInPids
238
+ ? `${pids.length} daemons for this project; lock holds pid ${lockHolder}, orphans: ${orphanPids.join(', ')}`
239
+ : `${pids.length} daemons for this project; no lock-holder identifiable, candidates: ${pids.join(', ')}`,
240
+ fix: 'flo healer --fix -c daemon-orphan',
241
+ };
242
+ }
105
243
  export async function checkMemoryDatabase() {
106
244
  const root = process.cwd();
107
245
  const canonical = memoryDbPath(root);
@@ -160,9 +160,29 @@ export async function autoFixCheck(check) {
160
160
  return false;
161
161
  }
162
162
  },
163
+ // #1150 — SIGTERM the lock-holder BEFORE unlinking the lock. The old
164
+ // shape (`unlink lock; daemon start`) is the bug that produced orphan
165
+ // daemon accumulation: if the lock-holder PID was still alive, the
166
+ // unlink left it running and the respawn produced a second same-project
167
+ // daemon. Mirrors the 'Daemon Version Skew' / 'Daemon Identity Match'
168
+ // shape which got this right.
169
+ //
170
+ // Also reaps any same-project orphans whose PIDs aren't recorded in the
171
+ // lock — those are the daemons that survived prior buggy fixes.
163
172
  'Daemon Status': async () => {
164
- const lockFile = join(process.cwd(), '.moflo', 'daemon.lock');
165
- const pidFile = join(process.cwd(), '.moflo', 'daemon.pid');
173
+ const cwd = process.cwd();
174
+ const { getDaemonLockPayload, reapSameProjectOrphans } = await import('../services/daemon-lock.js');
175
+ const payload = getDaemonLockPayload(cwd);
176
+ if (payload?.pid && payload.pid > 0) {
177
+ try {
178
+ process.kill(payload.pid, 'SIGTERM');
179
+ }
180
+ catch { /* already dead */ }
181
+ }
182
+ // Wipe other same-project daemons that the lock doesn't account for.
183
+ reapSameProjectOrphans(cwd);
184
+ const lockFile = join(cwd, '.moflo', 'daemon.lock');
185
+ const pidFile = join(cwd, '.moflo', 'daemon.pid');
166
186
  try {
167
187
  if (existsSync(lockFile))
168
188
  unlinkSync(lockFile);
@@ -195,6 +215,59 @@ export async function autoFixCheck(check) {
195
215
  catch { /* ok */ }
196
216
  return runFixCommand('npx moflo daemon start');
197
217
  },
218
+ // #1150 — terminate same-project orphan daemons. Keep the lock-holder
219
+ // alive if it shows up in the scan (it's the canonical daemon). If the
220
+ // lock-holder is missing/stale, kill all candidates and let the next
221
+ // session-start respawn a clean one. The pre-computed `pids` list is
222
+ // threaded into `reapSameProjectOrphans` so we don't re-run the
223
+ // OS process scan inside it.
224
+ 'Daemon Orphan': async () => {
225
+ const cwd = process.cwd();
226
+ const { findProjectDaemonPids, getDaemonLockHolder, reapSameProjectOrphans } = await import('../services/daemon-lock.js');
227
+ const pids = findProjectDaemonPids(cwd);
228
+ if (pids.length <= 1)
229
+ return true; // already healthy
230
+ const lockHolder = getDaemonLockHolder(cwd);
231
+ if (lockHolder != null && pids.includes(lockHolder)) {
232
+ const { survived } = reapSameProjectOrphans(cwd, process.pid, lockHolder, pids);
233
+ return survived.length === 0;
234
+ }
235
+ // No identifiable canonical daemon — kill them all, clear the lock,
236
+ // respawn fresh.
237
+ const { survived } = reapSameProjectOrphans(cwd, process.pid, undefined, pids);
238
+ const lockFile = join(cwd, '.moflo', 'daemon.lock');
239
+ try {
240
+ if (existsSync(lockFile))
241
+ unlinkSync(lockFile);
242
+ }
243
+ catch { /* ok */ }
244
+ if (survived.length > 0)
245
+ return false;
246
+ return runFixCommand('npx moflo daemon start');
247
+ },
248
+ // #1145 — daemon claims a different projectRoot than ours (or has no
249
+ // port in its lock so we can't verify). Same recycle pattern as version
250
+ // skew: SIGTERM the local daemon, clear the lock, respawn. Then the new
251
+ // daemon binds the per-project deterministic port and stamps it into
252
+ // the lock — clients can discover it without guessing.
253
+ 'Daemon Identity Match': async () => {
254
+ const cwd = process.cwd();
255
+ const { getDaemonLockPayload } = await import('../services/daemon-lock.js');
256
+ const payload = getDaemonLockPayload(cwd);
257
+ if (payload?.pid && payload.pid > 0) {
258
+ try {
259
+ process.kill(payload.pid, 'SIGTERM');
260
+ }
261
+ catch { /* already dead */ }
262
+ }
263
+ const lockFile = join(cwd, '.moflo', 'daemon.lock');
264
+ try {
265
+ if (existsSync(lockFile))
266
+ unlinkSync(lockFile);
267
+ }
268
+ catch { /* ok */ }
269
+ return runFixCommand('npx moflo daemon start');
270
+ },
198
271
  'Embedding Coverage Truth': async () => {
199
272
  // Same as the existing Embeddings fix — rebuild the cache by re-running
200
273
  // the embeddings pipeline. Routes through `npx moflo` so the consumer
@@ -12,7 +12,7 @@ import { checkWritersAudit } from './doctor-checks-writers-audit.js';
12
12
  import { checkSwarmFunctional, checkHiveMindFunctional, } from './doctor-checks-swarm.js';
13
13
  import { checkMemoryAccessFunctional } from './doctor-checks-memory-access.js';
14
14
  import { checkBuildTools, checkClaudeCode, checkDiskSpace, checkGit, checkGitRepo, checkNodeVersion, checkNpmVersion, } from './doctor-checks-runtime.js';
15
- import { checkConfigFile, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMofloYamlCompliance, checkStatusLine, checkTestDirs, } from './doctor-checks-config.js';
15
+ import { checkConfigFile, checkDaemonIdentity, checkDaemonOrphan, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMofloYamlCompliance, checkStatusLine, checkTestDirs, } from './doctor-checks-config.js';
16
16
  import { checkSpellEngine, checkSandboxTier } from './doctor-checks-platform.js';
17
17
  import { checkEmbeddings, checkSemanticQuality, } from './doctor-checks-memory.js';
18
18
  import { checkIntelligence } from './doctor-checks-intelligence.js';
@@ -37,6 +37,8 @@ export const allChecks = [
37
37
  checkStatusLine,
38
38
  checkDaemonStatus,
39
39
  checkDaemonVersionSkew,
40
+ checkDaemonIdentity,
41
+ checkDaemonOrphan,
40
42
  checkDaemonWriteRouting,
41
43
  checkWritersAudit,
42
44
  checkMemoryDatabase,
@@ -95,6 +97,13 @@ export const componentMap = {
95
97
  'skew': checkDaemonVersionSkew,
96
98
  'daemon-write-routing': checkDaemonWriteRouting,
97
99
  'write-routing': checkDaemonWriteRouting,
100
+ 'daemon-identity': checkDaemonIdentity,
101
+ 'daemon-identity-match': checkDaemonIdentity,
102
+ 'identity': checkDaemonIdentity,
103
+ 'daemon-orphan': checkDaemonOrphan,
104
+ 'daemon-orphans': checkDaemonOrphan,
105
+ 'orphan': checkDaemonOrphan,
106
+ 'orphans': checkDaemonOrphan,
98
107
  'writers-audit': checkWritersAudit,
99
108
  'writers': checkWritersAudit,
100
109
  'memory': checkMemoryDatabase,