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
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Daemon port resolution — single source of truth for the moflo daemon's
3
+ * HTTP port.
4
+ *
5
+ * Before #1145, `DEFAULT_DASHBOARD_PORT` in `daemon-dashboard.ts` and
6
+ * `DEFAULT_DAEMON_PORT` in `daemon-write-client.ts` were two separate `3117`
7
+ * literals. The server tried 3117 → 3126 on `EADDRINUSE`; the client always
8
+ * POSTed to 3117. When a second moflo project's daemon bound 3118+, that
9
+ * project's clients still hit 3117 → silent cross-project read/write
10
+ * routing. See `docs/internal/1145-daemon-port-collision-analysis.md`.
11
+ *
12
+ * This module collapses both literals into one resolver. Every entry point
13
+ * MUST go through `resolveProjectPort()` (or read the `port` field a
14
+ * already-bound daemon recorded in `.moflo/daemon.lock`).
15
+ *
16
+ * Resolution precedence — server and client agree:
17
+ * 1. `MOFLO_DAEMON_PORT` env override (consumer pin / smoke harness — wins)
18
+ * 2. Lock-file `port` field (client-only — server WRITES this after bind)
19
+ * 3. `resolveProjectPort(projectRoot)` — sha256(path) → 33000+(hash%1000)
20
+ * 4. `LEGACY_DEFAULT_PORT` (3117) — read-only fallback for ancient locks
21
+ * with no port field and no env override; warns once via stderr
22
+ *
23
+ * @module cli/services/daemon-port
24
+ */
25
+ import { createHash } from 'node:crypto';
26
+ import { existsSync, readFileSync, realpathSync } from 'node:fs';
27
+ import { join } from 'node:path';
28
+ import * as http from 'node:http';
29
+ /**
30
+ * Deterministic port range. 33000-33999 — clear of every common dev-server
31
+ * port (3000, 3001, 4000, 5000, 5173, 8000, 8080), every well-known service
32
+ * (≤ 1024), and the moflo legacy default (3117). Collision probability across
33
+ * N active projects is ~N/1000; the identity check (`isDaemonIdentityMatch`)
34
+ * is the safety net when collisions do hit.
35
+ */
36
+ export const PORT_RANGE_BASE = 33000;
37
+ export const PORT_RANGE_SIZE = 1000;
38
+ /**
39
+ * Legacy default port — used by daemons that haven't been upgraded past
40
+ * 4.10.7 and locks that never recorded a port field. Kept as a read-only
41
+ * fallback so a fresh client probing an old daemon still finds it; clients
42
+ * that fall through to this path emit a one-time deprecation warn.
43
+ *
44
+ * NEW code must NEVER reference this constant outside `daemon-port.ts`. The
45
+ * regression guard at `tests/system/no-fixed-3117.test.ts` enforces.
46
+ */
47
+ export const LEGACY_DEFAULT_PORT = 3117;
48
+ /**
49
+ * Resolve the canonical port for a given project root.
50
+ *
51
+ * Pure function — no I/O. Same project path → same port across daemon
52
+ * restarts, across processes, across machines (the hash is deterministic).
53
+ *
54
+ * @param projectRoot absolute path to the project root (use `findProjectRoot()`)
55
+ * @returns port in `[PORT_RANGE_BASE, PORT_RANGE_BASE + PORT_RANGE_SIZE)`
56
+ */
57
+ export function resolveProjectPort(projectRoot) {
58
+ const envPort = readEnvPortOverride();
59
+ if (envPort != null)
60
+ return envPort;
61
+ const hash = createHash('sha256').update(projectRoot).digest();
62
+ return PORT_RANGE_BASE + (hash.readUInt16BE(0) % PORT_RANGE_SIZE);
63
+ }
64
+ /**
65
+ * Read `MOFLO_DAEMON_PORT` from the environment. Returns the parsed port
66
+ * (1-65535) or `null` if unset/invalid.
67
+ *
68
+ * Exported so callers can short-circuit lock-file reads when the env is
69
+ * pinned — useful in the smoke harness and CI where the env is the
70
+ * authoritative pin.
71
+ */
72
+ export function readEnvPortOverride() {
73
+ const raw = process.env.MOFLO_DAEMON_PORT;
74
+ if (!raw)
75
+ return null;
76
+ const n = parseInt(raw, 10);
77
+ if (!Number.isFinite(n) || n < 1 || n > 65535)
78
+ return null;
79
+ return n;
80
+ }
81
+ /**
82
+ * Resolve the daemon port a CLIENT should connect to for a given project.
83
+ *
84
+ * Reads `.moflo/daemon.lock` to discover the actual bound port — if the
85
+ * daemon collided with another project in its deterministic-range bucket
86
+ * and the dashboard retry loop bumped it forward, the lock reflects reality
87
+ * and the client follows. Falls back to `resolveProjectPort` when the lock
88
+ * is absent (daemon not yet started), the lock has no `port` field (old
89
+ * daemon predating #1145), or the port reads as invalid.
90
+ *
91
+ * Never throws — every I/O failure degrades to the deterministic fallback.
92
+ */
93
+ export function resolveClientPort(projectRoot) {
94
+ const envPort = readEnvPortOverride();
95
+ if (envPort != null)
96
+ return envPort;
97
+ try {
98
+ const lockFile = join(projectRoot, '.moflo', 'daemon.lock');
99
+ if (existsSync(lockFile)) {
100
+ const raw = readFileSync(lockFile, 'utf-8');
101
+ const lock = JSON.parse(raw);
102
+ const lockPort = typeof lock?.port === 'number' ? lock.port : null;
103
+ if (lockPort && Number.isFinite(lockPort) && lockPort > 0 && lockPort < 65536) {
104
+ return lockPort;
105
+ }
106
+ }
107
+ }
108
+ catch {
109
+ // Corrupt or unreadable lock — fall through to deterministic port.
110
+ }
111
+ return resolveProjectPort(projectRoot);
112
+ }
113
+ /**
114
+ * Build the list of ports the SERVER should try, in order, when starting
115
+ * the daemon. First entry is the deterministic port; the rest are the
116
+ * collision-fallback range. Capped at `PORT_RANGE_SIZE` so the loop can
117
+ * never wrap past the bucket.
118
+ *
119
+ * If the env override is set, the list collapses to that single port —
120
+ * the consumer pinned it on purpose; respect their choice and hard-fail
121
+ * if it's already in use.
122
+ */
123
+ export function serverPortCandidates(projectRoot, maxAttempts = 10) {
124
+ const envPort = readEnvPortOverride();
125
+ if (envPort != null)
126
+ return [envPort];
127
+ const base = resolveProjectPort(projectRoot);
128
+ const attempts = Math.min(Math.max(1, maxAttempts), PORT_RANGE_SIZE);
129
+ const ports = [];
130
+ for (let i = 0; i < attempts; i++) {
131
+ ports.push(PORT_RANGE_BASE + ((base - PORT_RANGE_BASE + i) % PORT_RANGE_SIZE));
132
+ }
133
+ return ports;
134
+ }
135
+ // ============================================================================
136
+ // Identity probe — shared by client + healer (#1145)
137
+ // ============================================================================
138
+ /**
139
+ * Normalize project root paths for identity comparison.
140
+ *
141
+ * - Resolve symlinks via `realpathSync`. macOS aliases `/var/folders`
142
+ * → `/private/var/folders`; one side of the daemon/client pair may
143
+ * resolve the symlink and the other may not, producing false-positive
144
+ * identity mismatches on otherwise-matching project roots (caught by
145
+ * the consumer-smoke harness on macOS + Ubuntu after the original
146
+ * #1145 fix). Ubuntu hits the same shape via `/tmp` symlinks under
147
+ * certain mount configurations.
148
+ * - Lowercase on Windows so `C:\Users\...` and `c:\users\...` compare
149
+ * equal. POSIX is case-sensitive — pass through.
150
+ *
151
+ * Never throws — a path that doesn't exist (or that we lack permission
152
+ * to stat) falls back to the input string. The fallback case is safe
153
+ * because the symlink-mismatch class only fires on paths that DO exist
154
+ * (both daemon and client just resolved them).
155
+ */
156
+ export function normalizeProjectRoot(p) {
157
+ let resolved = p;
158
+ try {
159
+ resolved = realpathSync(p);
160
+ }
161
+ catch { /* path doesn't exist / EACCES — use input */ }
162
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
163
+ }
164
+ /**
165
+ * Send `GET /api/health` to `127.0.0.1:<port>` and parse the daemon's
166
+ * identity payload. Never throws — every failure mode maps to a
167
+ * `DaemonIdentityProbe` variant.
168
+ *
169
+ * Shared by `daemon-write-client.ts` (per-request safety net) and
170
+ * `doctor-checks-config.ts` (`checkDaemonIdentity` subcheck).
171
+ */
172
+ export function probeDaemonHealth(port, timeoutMs) {
173
+ return new Promise((resolve) => {
174
+ let done = false;
175
+ const finish = (r) => {
176
+ if (done)
177
+ return;
178
+ done = true;
179
+ resolve(r);
180
+ };
181
+ const req = http.get({ host: '127.0.0.1', port, path: '/api/health', timeout: timeoutMs }, (res) => {
182
+ const status = res.statusCode ?? 0;
183
+ if (status === 404) {
184
+ res.resume();
185
+ finish({ kind: 'legacy' });
186
+ return;
187
+ }
188
+ if (status !== 200) {
189
+ res.resume();
190
+ finish({ kind: 'unreachable' });
191
+ return;
192
+ }
193
+ let buf = '';
194
+ res.setEncoding('utf8');
195
+ res.on('data', (chunk) => { buf += chunk; });
196
+ res.on('end', () => {
197
+ try {
198
+ const data = JSON.parse(buf);
199
+ if (typeof data?.projectRoot === 'string' && data.projectRoot.length > 0) {
200
+ finish({ kind: 'identity', projectRoot: data.projectRoot });
201
+ return;
202
+ }
203
+ // 200 but no identity field — pre-#1145 daemon that 200s on
204
+ // every URL. Same handling as a 404.
205
+ finish({ kind: 'legacy' });
206
+ }
207
+ catch {
208
+ finish({ kind: 'legacy' });
209
+ }
210
+ });
211
+ res.on('error', () => finish({ kind: 'unreachable' }));
212
+ });
213
+ req.on('error', () => finish({ kind: 'unreachable' }));
214
+ req.on('timeout', () => { req.destroy(); finish({ kind: 'unreachable' }); });
215
+ });
216
+ }
217
+ //# sourceMappingURL=daemon-port.js.map
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.10.7';
5
+ export const VERSION = '4.10.8';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.10.7",
3
+ "version": "4.10.8",
4
4
  "description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
5
5
  "main": "dist/src/cli/index.js",
6
6
  "type": "module",
@@ -95,7 +95,7 @@
95
95
  "@typescript-eslint/eslint-plugin": "^7.18.0",
96
96
  "@typescript-eslint/parser": "^7.18.0",
97
97
  "eslint": "^8.0.0",
98
- "moflo": "^4.10.6",
98
+ "moflo": "^4.10.7",
99
99
  "tsx": "^4.21.0",
100
100
  "typescript": "^5.9.3",
101
101
  "vitest": "^4.0.0"
@@ -1,182 +0,0 @@
1
- /**
2
- * Configuration Adapter
3
- * Converts between SystemConfig and V3Config types
4
- */
5
- /**
6
- * Convert SystemConfig to V3Config (CLI-specific format)
7
- */
8
- export function systemConfigToV3Config(systemConfig) {
9
- return {
10
- version: '3.0.0',
11
- projectRoot: systemConfig.orchestrator?.session?.dataDir || process.cwd(),
12
- // Agent configuration
13
- agents: {
14
- defaultType: 'coder',
15
- autoSpawn: false, // Not in SystemConfig
16
- maxConcurrent: systemConfig.orchestrator?.lifecycle?.maxConcurrentAgents ?? 15,
17
- timeout: systemConfig.orchestrator?.lifecycle?.spawnTimeout ?? 300000,
18
- providers: [],
19
- },
20
- // Swarm configuration
21
- swarm: {
22
- topology: normalizeTopology(systemConfig.swarm?.topology),
23
- maxAgents: systemConfig.swarm?.maxAgents ?? 15,
24
- autoScale: systemConfig.swarm?.autoScale?.enabled ?? false,
25
- coordinationStrategy: systemConfig.swarm?.coordination?.consensusRequired ? 'consensus' : 'leader',
26
- healthCheckInterval: systemConfig.swarm?.coordination?.timeoutMs ?? 10000,
27
- },
28
- // Memory configuration
29
- memory: {
30
- backend: normalizeMemoryBackend(systemConfig.memory?.type),
31
- persistPath: systemConfig.memory?.path || './data/memory',
32
- cacheSize: systemConfig.memory?.maxSize ?? 1000000,
33
- enableHNSW: systemConfig.memory?.agentdb?.indexType === 'hnsw',
34
- vectorDimension: systemConfig.memory?.agentdb?.dimensions ?? 1536,
35
- },
36
- // MCP configuration (only stdio transport is supported)
37
- mcp: {
38
- autoStart: false, // Not in SystemConfig
39
- transportType: 'stdio',
40
- tools: [], // Not in SystemConfig
41
- },
42
- // CLI preferences
43
- cli: {
44
- colorOutput: true,
45
- interactive: true,
46
- verbosity: 'normal',
47
- outputFormat: 'text',
48
- progressStyle: 'spinner',
49
- },
50
- // Hooks configuration
51
- hooks: {
52
- enabled: false,
53
- autoExecute: false,
54
- hooks: [],
55
- },
56
- };
57
- }
58
- /**
59
- * Convert V3Config to SystemConfig
60
- */
61
- export function v3ConfigToSystemConfig(v3Config) {
62
- return {
63
- orchestrator: {
64
- lifecycle: {
65
- maxConcurrentAgents: v3Config.agents.maxConcurrent,
66
- spawnTimeout: v3Config.agents.timeout,
67
- terminateTimeout: 10000,
68
- maxSpawnRetries: 3,
69
- },
70
- session: {
71
- dataDir: v3Config.projectRoot,
72
- persistSessions: true,
73
- sessionRetentionMs: 3600000,
74
- },
75
- health: {
76
- checkInterval: v3Config.swarm.healthCheckInterval,
77
- historyLimit: 100,
78
- degradedThreshold: 1,
79
- unhealthyThreshold: 2,
80
- },
81
- },
82
- swarm: {
83
- topology: denormalizeTopology(v3Config.swarm.topology),
84
- maxAgents: v3Config.swarm.maxAgents,
85
- autoScale: {
86
- enabled: v3Config.swarm.autoScale,
87
- minAgents: 1,
88
- maxAgents: v3Config.swarm.maxAgents,
89
- scaleUpThreshold: 0.8,
90
- scaleDownThreshold: 0.3,
91
- },
92
- coordination: {
93
- consensusRequired: v3Config.swarm.coordinationStrategy === 'consensus',
94
- timeoutMs: v3Config.swarm.healthCheckInterval,
95
- retryPolicy: {
96
- maxRetries: 3,
97
- backoffMs: 500,
98
- },
99
- },
100
- communication: {
101
- protocol: 'events',
102
- batchSize: 10,
103
- flushIntervalMs: 100,
104
- },
105
- },
106
- memory: {
107
- type: denormalizeMemoryBackend(v3Config.memory.backend),
108
- path: v3Config.memory.persistPath,
109
- maxSize: v3Config.memory.cacheSize,
110
- agentdb: {
111
- dimensions: v3Config.memory.vectorDimension,
112
- indexType: v3Config.memory.enableHNSW ? 'hnsw' : 'flat',
113
- efConstruction: 200,
114
- m: 16,
115
- quantization: 'none',
116
- },
117
- },
118
- mcp: {
119
- name: 'moflo',
120
- version: '3.0.0',
121
- transport: {
122
- type: 'stdio',
123
- },
124
- capabilities: {
125
- tools: true,
126
- resources: true,
127
- prompts: true,
128
- logging: true,
129
- },
130
- },
131
- };
132
- }
133
- /**
134
- * Normalize topology from SystemConfig to V3Config
135
- */
136
- function normalizeTopology(topology) {
137
- switch (topology) {
138
- case 'hierarchical':
139
- case 'mesh':
140
- case 'ring':
141
- case 'star':
142
- case 'hybrid':
143
- case 'hierarchical-mesh':
144
- return topology;
145
- case 'adaptive':
146
- return 'hybrid';
147
- default:
148
- return 'hierarchical';
149
- }
150
- }
151
- /**
152
- * Denormalize topology from V3Config to SystemConfig
153
- */
154
- function denormalizeTopology(topology) {
155
- if (topology === 'hybrid') {
156
- return 'hierarchical-mesh';
157
- }
158
- return topology;
159
- }
160
- /**
161
- * Normalize memory backend from SystemConfig to V3Config
162
- */
163
- function normalizeMemoryBackend(backend) {
164
- switch (backend) {
165
- case 'memory':
166
- case 'sqlite':
167
- case 'agentdb':
168
- case 'hybrid':
169
- return backend;
170
- case 'redis':
171
- return 'memory'; // Redis maps to memory for CLI purposes
172
- default:
173
- return 'hybrid';
174
- }
175
- }
176
- /**
177
- * Denormalize memory backend from V3Config to SystemConfig
178
- */
179
- function denormalizeMemoryBackend(backend) {
180
- return backend;
181
- }
182
- //# sourceMappingURL=config-adapter.js.map