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.
- package/.claude/guidance/shipped/moflo-cli-reference.md +1 -1
- package/.claude/guidance/shipped/moflo-memory-strategy.md +1 -1
- package/.claude/guidance/shipped/moflo-yaml-reference.md +4 -4
- package/.claude/skills/memory-optimization/SKILL.md +1 -1
- package/.claude/skills/memory-patterns/SKILL.md +3 -3
- package/.claude/skills/vector-search/SKILL.md +2 -2
- package/README.md +5 -5
- package/bin/hooks.mjs +3 -2
- package/bin/index-all.mjs +3 -2
- package/bin/index-guidance.mjs +4 -4
- package/bin/lib/daemon-port.mjs +66 -0
- package/bin/lib/process-manager.mjs +3 -3
- package/dist/src/cli/commands/daemon.js +31 -10
- package/dist/src/cli/commands/doctor-checks-config.js +182 -10
- package/dist/src/cli/commands/doctor-fixes.js +208 -3
- package/dist/src/cli/commands/doctor-registry.js +16 -1
- package/dist/src/cli/commands/memory.js +8 -8
- package/dist/src/cli/commands/neural.js +8 -6
- package/dist/src/cli/config/moflo-config.js +68 -3
- package/dist/src/cli/index.js +18 -19
- package/dist/src/cli/init/moflo-yaml-template.js +1 -1
- package/dist/src/cli/mcp-server.js +59 -10
- package/dist/src/cli/mcp-tools/memory-tools.js +46 -27
- package/dist/src/cli/memory/auto-memory-bridge.js +1 -1
- package/dist/src/cli/memory/controllers/attestation-log.js +1 -1
- package/dist/src/cli/memory/controllers/causal-graph.js +1 -1
- package/dist/src/cli/memory/daemon-write-client.js +178 -49
- package/dist/src/cli/memory/database-provider.js +58 -3
- package/dist/src/cli/memory/intelligence.js +54 -26
- package/dist/src/cli/memory/memory-initializer.js +21 -11
- package/dist/src/cli/movector/model-router.js +1 -1
- package/dist/src/cli/movector/q-learning-router.js +2 -2
- package/dist/src/cli/services/daemon-dashboard.js +94 -25
- package/dist/src/cli/services/daemon-lock.js +390 -3
- package/dist/src/cli/services/daemon-port.js +252 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- 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 |
|
|
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
|
|
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:
|
|
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=
|
|
161
|
-
CLAUDE_FLOW_MEMORY_TYPE=sqlite #
|
|
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.**
|
|
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
|
|
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 `.
|
|
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: `.
|
|
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 (
|
|
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 `.
|
|
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
|
-
- **
|
|
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 (
|
|
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 (
|
|
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
|
|
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,
|
|
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, '.
|
|
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 .
|
|
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, '.
|
|
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);
|
package/bin/index-guidance.mjs
CHANGED
|
@@ -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 `.
|
|
849
|
-
//
|
|
850
|
-
//
|
|
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: .
|
|
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
|
|
168
|
-
ensureDir(
|
|
169
|
-
const logPath = resolve(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
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
|