moflo 4.10.7 → 4.10.9

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 (38) 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/hooks.mjs +3 -2
  9. package/bin/index-all.mjs +3 -2
  10. package/bin/index-guidance.mjs +4 -4
  11. package/bin/lib/daemon-port.mjs +66 -0
  12. package/bin/lib/process-manager.mjs +3 -3
  13. package/dist/src/cli/commands/daemon.js +31 -10
  14. package/dist/src/cli/commands/doctor-checks-config.js +182 -10
  15. package/dist/src/cli/commands/doctor-fixes.js +208 -3
  16. package/dist/src/cli/commands/doctor-registry.js +16 -1
  17. package/dist/src/cli/commands/memory.js +8 -8
  18. package/dist/src/cli/commands/neural.js +8 -6
  19. package/dist/src/cli/config/moflo-config.js +68 -3
  20. package/dist/src/cli/index.js +18 -19
  21. package/dist/src/cli/init/moflo-yaml-template.js +1 -1
  22. package/dist/src/cli/mcp-server.js +59 -10
  23. package/dist/src/cli/mcp-tools/memory-tools.js +46 -27
  24. package/dist/src/cli/memory/auto-memory-bridge.js +1 -1
  25. package/dist/src/cli/memory/controllers/attestation-log.js +1 -1
  26. package/dist/src/cli/memory/controllers/causal-graph.js +1 -1
  27. package/dist/src/cli/memory/daemon-write-client.js +178 -49
  28. package/dist/src/cli/memory/database-provider.js +58 -3
  29. package/dist/src/cli/memory/intelligence.js +54 -26
  30. package/dist/src/cli/memory/memory-initializer.js +21 -11
  31. package/dist/src/cli/movector/model-router.js +1 -1
  32. package/dist/src/cli/movector/q-learning-router.js +2 -2
  33. package/dist/src/cli/services/daemon-dashboard.js +94 -25
  34. package/dist/src/cli/services/daemon-lock.js +390 -3
  35. package/dist/src/cli/services/daemon-port.js +252 -0
  36. package/dist/src/cli/version.js +1 -1
  37. package/package.json +2 -2
  38. 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
 
package/bin/hooks.mjs CHANGED
@@ -36,14 +36,15 @@ const __dirname = dirname(__filename);
36
36
  // projects, so __dirname-relative paths break. findProjectRoot() works
37
37
  // everywhere and resolves identically to the TS bridge (see lib/moflo-paths.mjs).
38
38
  const projectRoot = findProjectRoot();
39
- const logFile = resolve(projectRoot, '.swarm/hooks.log');
39
+ const logFile = resolve(projectRoot, '.moflo', 'logs', 'hooks.log');
40
+ try { mkdirSync(dirname(logFile), { recursive: true }); } catch { /* best effort */ }
40
41
  const pm = createProcessManager(projectRoot);
41
42
 
42
43
  // Parse command line args
43
44
  const args = process.argv.slice(2);
44
45
  const hookType = args[0];
45
46
 
46
- // Simple log function - writes to .swarm/hooks.log
47
+ // Simple log function - writes to .moflo/logs/hooks.log
47
48
  function log(level, message) {
48
49
  const timestamp = new Date().toISOString();
49
50
  const line = `[${timestamp}] [${level.toUpperCase()}] [${hookType || 'unknown'}] ${message}\n`;
package/bin/index-all.mjs CHANGED
@@ -13,7 +13,7 @@
13
13
  * Spawned as a single detached background process by hooks.mjs session-start.
14
14
  */
15
15
 
16
- import { existsSync, appendFileSync, readFileSync } from 'fs';
16
+ import { existsSync, appendFileSync, readFileSync, mkdirSync } from 'fs';
17
17
  import { resolve, dirname } from 'path';
18
18
  import { fileURLToPath } from 'url';
19
19
  import { spawn, spawnSync } from 'child_process';
@@ -43,7 +43,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
43
43
  // so __dirname-relative paths break. findProjectRoot() (lib/moflo-paths.mjs)
44
44
  // works in both locations and resolves identically to the TS bridge.
45
45
  const projectRoot = findProjectRoot();
46
- const LOG_PATH = resolve(projectRoot, '.swarm/hooks.log');
46
+ const LOG_PATH = resolve(projectRoot, '.moflo', 'logs', 'hooks.log');
47
+ try { mkdirSync(dirname(LOG_PATH), { recursive: true }); } catch { /* best effort */ }
47
48
 
48
49
  function log(msg) {
49
50
  const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
@@ -845,16 +845,16 @@ if (!skipEmbeddings && needsEmbeddings) {
845
845
 
846
846
  if (embeddingScript) {
847
847
  // Register the spawn with the shared ProcessManager (#886). Stdout/stderr
848
- // route through `.swarm/background.log` (pm.spawn default) instead of the
849
- // bespoke `.moflo/logs/embeddings.log` so the registry, dedup, and
850
- // session-end drain stay consistent with every other tracked spawn.
848
+ // route through `.moflo/logs/background.log` (pm.spawn default) so the
849
+ // registry, dedup, and session-end drain stay consistent with every other
850
+ // tracked spawn.
851
851
  const pm = createProcessManager(projectRoot);
852
852
  const result = pm.spawn('node', [embeddingScript, '--namespace', NAMESPACE], `build-embeddings-${NAMESPACE}`);
853
853
  if (result.skipped) {
854
854
  log(`Background embedding already running (PID: ${result.pid})`);
855
855
  } else if (result.pid) {
856
856
  log(`Background embedding started (PID: ${result.pid})`);
857
- log(`Log file: .swarm/background.log`);
857
+ log(`Log file: .moflo/logs/background.log`);
858
858
  } else {
859
859
  log('⚠️ Failed to spawn background embedding');
860
860
  }
@@ -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
+ }
@@ -164,9 +164,9 @@ export function createProcessManager(root) {
164
164
  // This ensures errors from background indexers/pretrain are captured
165
165
  let stdio = 'ignore';
166
166
  try {
167
- const swarmDir = resolve(projectRoot, '.swarm');
168
- ensureDir(swarmDir);
169
- const logPath = resolve(swarmDir, 'background.log');
167
+ const logsDir = resolve(projectRoot, '.moflo', 'logs');
168
+ ensureDir(logsDir);
169
+ const logPath = resolve(logsDir, 'background.log');
170
170
  const fd = openSync(logPath, 'a');
171
171
  stdio = ['ignore', fd, fd];
172
172
  } catch {
@@ -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,8 +6,9 @@
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';
10
- import { legacyMemoryDbPath, memoryDbCandidatePaths, memoryDbPath, } from '../services/moflo-paths.js';
9
+ import { findProjectDaemonPids, getDaemonLockHolder, getDaemonLockPayload, } from '../services/daemon-lock.js';
10
+ import { resolveClientPort, LEGACY_DEFAULT_PORT, probeDaemonHealthWithRetry as probeDaemonHealthIdentity, normalizeProjectRoot, } from '../services/daemon-port.js';
11
+ import { LEGACY_SWARM_DIR, 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';
13
14
  import { errorDetail } from '../shared/utils/error-detail.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);
@@ -115,14 +253,10 @@ export async function checkMemoryDatabase() {
115
253
  }
116
254
  const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
117
255
  if (dbPath === canonical) {
118
- let message = `.moflo/moflo.db (${sizeMB} MB)`;
119
- // Unfinished migration tail: source still present means the launcher's
120
- // rename-to-.bak step failed (Windows lock most often). Flag so the user
121
- // knows to clear the stale source.
122
- if (existsSync(legacyMemoryDbPath(root))) {
123
- message += ' — legacy .swarm/memory.db still present (delete it after confirming canonical is healthy)';
124
- }
125
- return { name: 'Memory Database', status: 'pass', message };
256
+ // Legacy `.swarm/memory.db` residue is owned by the separate
257
+ // `checkSwarmResidue` check so we keep this check focused on the
258
+ // canonical DB. That check carries the auto-fix.
259
+ return { name: 'Memory Database', status: 'pass', message: `.moflo/moflo.db (${sizeMB} MB)` };
126
260
  }
127
261
  return {
128
262
  name: 'Memory Database',
@@ -133,6 +267,44 @@ export async function checkMemoryDatabase() {
133
267
  }
134
268
  return { name: 'Memory Database', status: 'warn', message: 'Not initialized', fix: 'claude-flow memory configure --backend hybrid' };
135
269
  }
270
+ /**
271
+ * Catches `.swarm/` residue that survived past the canonical migration:
272
+ * - `memory.db` / `memory.db.bak` — stale once `.moflo/moflo.db` exists.
273
+ * - `q-learning-model.json` / `model-router-state.json` — live router state
274
+ * that pre-dates the `.moflo/movector/` defaults; migrate, don't delete.
275
+ * - `hooks.log` / `background.log` — diagnostic logs the launcher used to
276
+ * route to `.swarm/`; relocate to `.moflo/logs/`.
277
+ *
278
+ * Passes when `.swarm/` is absent OR contains nothing the migrator recognises.
279
+ * Otherwise warns with `fix: 'flo healer --fix -c swarm-residue'` so the auto-fix
280
+ * dispatcher (`fixSwarmLegacyResidue` in doctor-fixes.ts) can clean it up in
281
+ * one pass.
282
+ */
283
+ export async function checkSwarmResidue() {
284
+ const root = findProjectRoot();
285
+ const swarmDir = join(root, LEGACY_SWARM_DIR);
286
+ if (!existsSync(swarmDir)) {
287
+ return { name: 'Swarm Residue', status: 'pass', message: 'No .swarm/ directory present' };
288
+ }
289
+ const artifacts = [
290
+ 'memory.db',
291
+ 'memory.db.bak',
292
+ 'q-learning-model.json',
293
+ 'model-router-state.json',
294
+ 'hooks.log',
295
+ 'background.log',
296
+ ];
297
+ const present = artifacts.filter(name => existsSync(join(swarmDir, name)));
298
+ if (present.length === 0) {
299
+ return { name: 'Swarm Residue', status: 'pass', message: '.swarm/ present but no known residue' };
300
+ }
301
+ return {
302
+ name: 'Swarm Residue',
303
+ status: 'warn',
304
+ message: `${present.length} legacy artifact(s) in .swarm/: ${present.join(', ')}`,
305
+ fix: 'flo healer --fix -c swarm-residue',
306
+ };
307
+ }
136
308
  /**
137
309
  * Tier-1 corruption probe for `.moflo/moflo.db`. Runs `PRAGMA integrity_check`
138
310
  * via a raw node:sqlite readonly handle — bypasses `openBackend` because that