moflo 4.10.6 → 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.
- 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 +19 -5
- 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/lib/daemon-port.mjs +66 -0
- package/bin/session-start-launcher.mjs +189 -15
- package/bin/setup-project.mjs +38 -58
- package/dist/src/cli/commands/daemon.js +31 -10
- package/dist/src/cli/commands/doctor-checks-config.js +139 -1
- package/dist/src/cli/commands/doctor-checks-deep.js +105 -0
- package/dist/src/cli/commands/doctor-fixes.js +99 -2
- package/dist/src/cli/commands/doctor-registry.js +15 -2
- 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 +79 -3
- package/dist/src/cli/index.js +18 -19
- package/dist/src/cli/init/claudemd-generator.js +6 -2
- package/dist/src/cli/init/moflo-init.js +13 -21
- 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/services/claudemd-injection.js +173 -0
- 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 +217 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/dist/src/cli/config-adapter.js +0 -182
|
@@ -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,7 +6,8 @@
|
|
|
6
6
|
import { existsSync, readFileSync, statSync } from 'fs';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import os from 'os';
|
|
9
|
-
import { getDaemonLockHolder } from '../services/daemon-lock.js';
|
|
9
|
+
import { findProjectDaemonPids, getDaemonLockHolder, getDaemonLockPayload, } from '../services/daemon-lock.js';
|
|
10
|
+
import { resolveClientPort, LEGACY_DEFAULT_PORT, probeDaemonHealth as probeDaemonHealthIdentity, normalizeProjectRoot, } from '../services/daemon-port.js';
|
|
10
11
|
import { legacyMemoryDbPath, memoryDbCandidatePaths, memoryDbPath, } from '../services/moflo-paths.js';
|
|
11
12
|
import { probeDbIntegrity } from '../services/memory-db-integrity-repair.js';
|
|
12
13
|
import { findProjectRoot } from '../services/project-root.js';
|
|
@@ -102,6 +103,143 @@ export async function checkDaemonStatus() {
|
|
|
102
103
|
return { name: 'Daemon Status', status: 'warn', message: `Unable to check: ${errorDetail(e)}`, fix: 'claude-flow daemon status' };
|
|
103
104
|
}
|
|
104
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Daemon identity check (#1145).
|
|
108
|
+
*
|
|
109
|
+
* Reads `.moflo/daemon.lock` → probes `/api/health` on the recorded port →
|
|
110
|
+
* confirms the daemon's reported `projectRoot` matches `findProjectRoot()`.
|
|
111
|
+
* Catches the silent cross-project routing class where two moflo daemons
|
|
112
|
+
* (e.g. moflo-dev + a consumer) share a port and the client hits the wrong
|
|
113
|
+
* one.
|
|
114
|
+
*
|
|
115
|
+
* Status semantics:
|
|
116
|
+
* - `pass` — no daemon (cleanly absent) OR identity matches.
|
|
117
|
+
* - `warn` — daemon has no port in lock (pre-#1145 daemon; harmless if
|
|
118
|
+
* no collision, but should be recycled to upgrade).
|
|
119
|
+
* - `fail` — `/api/health` reports a different project. Routing is
|
|
120
|
+
* polluted; the healer fix kills the foreign-identity daemon and
|
|
121
|
+
* respawns the local one.
|
|
122
|
+
*/
|
|
123
|
+
export async function checkDaemonIdentity(cwd = process.cwd()) {
|
|
124
|
+
const projectRoot = findProjectRoot({ cwd });
|
|
125
|
+
const payload = getDaemonLockPayload(projectRoot);
|
|
126
|
+
if (!payload) {
|
|
127
|
+
return {
|
|
128
|
+
name: 'Daemon Identity Match',
|
|
129
|
+
status: 'pass',
|
|
130
|
+
message: 'No daemon running (nothing to verify)',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Lock present but no port field — pre-#1145 daemon. Identity check is
|
|
134
|
+
// best-effort; surface as warn so the user knows to recycle but don't
|
|
135
|
+
// hard-fail unless we can prove cross-project routing.
|
|
136
|
+
const portFromLock = payload.port;
|
|
137
|
+
const probePort = portFromLock ?? resolveClientPort(projectRoot);
|
|
138
|
+
if (!portFromLock) {
|
|
139
|
+
// Try the legacy default explicitly so consumers running an
|
|
140
|
+
// un-upgraded daemon still get a useful diagnostic.
|
|
141
|
+
const probe = await probeDaemonHealthIdentity(LEGACY_DEFAULT_PORT, 1500);
|
|
142
|
+
if (probe.kind === 'identity'
|
|
143
|
+
&& normalizeProjectRoot(probe.projectRoot) !== normalizeProjectRoot(projectRoot)) {
|
|
144
|
+
return {
|
|
145
|
+
name: 'Daemon Identity Match',
|
|
146
|
+
status: 'fail',
|
|
147
|
+
message: `Daemon at 127.0.0.1:${LEGACY_DEFAULT_PORT} claims project '${probe.projectRoot}' — cross-project routing active`,
|
|
148
|
+
fix: 'flo healer --fix -c daemon-identity',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
name: 'Daemon Identity Match',
|
|
153
|
+
status: 'warn',
|
|
154
|
+
message: 'Daemon lock has no port field — pre-#1145 daemon; recycle to enable port discovery',
|
|
155
|
+
fix: 'flo healer --fix -c daemon-identity',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const probe = await probeDaemonHealthIdentity(probePort, 1500);
|
|
159
|
+
if (probe.kind === 'unreachable') {
|
|
160
|
+
return {
|
|
161
|
+
name: 'Daemon Identity Match',
|
|
162
|
+
status: 'warn',
|
|
163
|
+
message: `Daemon at 127.0.0.1:${probePort} unreachable on /api/health`,
|
|
164
|
+
fix: 'flo healer --fix -c daemon-identity',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (probe.kind === 'legacy') {
|
|
168
|
+
return {
|
|
169
|
+
name: 'Daemon Identity Match',
|
|
170
|
+
status: 'warn',
|
|
171
|
+
message: `Daemon at 127.0.0.1:${probePort} has no /api/health (legacy) — recycle to upgrade`,
|
|
172
|
+
fix: 'flo healer --fix -c daemon-identity',
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
if (normalizeProjectRoot(probe.projectRoot) !== normalizeProjectRoot(projectRoot)) {
|
|
176
|
+
return {
|
|
177
|
+
name: 'Daemon Identity Match',
|
|
178
|
+
status: 'fail',
|
|
179
|
+
message: `Daemon at 127.0.0.1:${probePort} claims project '${probe.projectRoot}' but cwd is '${projectRoot}'`,
|
|
180
|
+
fix: 'flo healer --fix -c daemon-identity',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
name: 'Daemon Identity Match',
|
|
185
|
+
status: 'pass',
|
|
186
|
+
message: `OK (port ${probePort}, pid ${payload.pid})`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Same-project orphan daemon check (#1150).
|
|
191
|
+
*
|
|
192
|
+
* Counts moflo daemon processes whose command line is rooted at THIS
|
|
193
|
+
* project's CLI binary. Healthy state: 0 (no daemon) or 1 (the lock-holder).
|
|
194
|
+
* Failure state: >1 — multiple daemons are racing for the indexer lock and
|
|
195
|
+
* writing to the same `daemon-state.json`.
|
|
196
|
+
*
|
|
197
|
+
* Distinct from `Daemon Identity Match`, which catches a DIFFERENT project's
|
|
198
|
+
* daemon answering on a port. This check catches multiple SAME-project
|
|
199
|
+
* daemons sharing the project root but with one of them holding a stale or
|
|
200
|
+
* unlinked lock (the orphan path).
|
|
201
|
+
*
|
|
202
|
+
* Status semantics:
|
|
203
|
+
* - `pass` — 0 or 1 same-project daemon.
|
|
204
|
+
* - `fail` — 2+ same-project daemons. Auto-fix terminates all but the
|
|
205
|
+
* lock-recorded PID (or all + respawn if no lock matches a live one).
|
|
206
|
+
* The "no live lock-holder" sub-case stays `fail` rather than `warn`:
|
|
207
|
+
* a stale lock alongside live orphan daemons is a strictly worse state
|
|
208
|
+
* than an orphan that the lock knows about, not a softer one.
|
|
209
|
+
* - `warn` — the OS process scan itself failed (platform introspection
|
|
210
|
+
* unavailable). The healer is offered as a fallback but isn't binding.
|
|
211
|
+
*/
|
|
212
|
+
export async function checkDaemonOrphan(cwd = process.cwd()) {
|
|
213
|
+
const projectRoot = findProjectRoot({ cwd });
|
|
214
|
+
let pids;
|
|
215
|
+
try {
|
|
216
|
+
pids = findProjectDaemonPids(projectRoot);
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
return {
|
|
220
|
+
name: 'Daemon Orphan',
|
|
221
|
+
status: 'warn',
|
|
222
|
+
message: `Process scan failed: ${errorDetail(e)}`,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (pids.length === 0) {
|
|
226
|
+
return { name: 'Daemon Orphan', status: 'pass', message: 'No daemon running (nothing to verify)' };
|
|
227
|
+
}
|
|
228
|
+
if (pids.length === 1) {
|
|
229
|
+
return { name: 'Daemon Orphan', status: 'pass', message: `1 daemon (pid ${pids[0]})` };
|
|
230
|
+
}
|
|
231
|
+
const lockHolder = getDaemonLockHolder(projectRoot);
|
|
232
|
+
const lockHolderInPids = lockHolder != null && pids.includes(lockHolder);
|
|
233
|
+
const orphanPids = lockHolderInPids ? pids.filter(p => p !== lockHolder) : pids;
|
|
234
|
+
return {
|
|
235
|
+
name: 'Daemon Orphan',
|
|
236
|
+
status: 'fail',
|
|
237
|
+
message: lockHolderInPids
|
|
238
|
+
? `${pids.length} daemons for this project; lock holds pid ${lockHolder}, orphans: ${orphanPids.join(', ')}`
|
|
239
|
+
: `${pids.length} daemons for this project; no lock-holder identifiable, candidates: ${pids.join(', ')}`,
|
|
240
|
+
fix: 'flo healer --fix -c daemon-orphan',
|
|
241
|
+
};
|
|
242
|
+
}
|
|
105
243
|
export async function checkMemoryDatabase() {
|
|
106
244
|
const root = process.cwd();
|
|
107
245
|
const canonical = memoryDbPath(root);
|
|
@@ -670,4 +670,109 @@ export async function checkHookBlockDrift() {
|
|
|
670
670
|
fix: 'set auto_update.hook_block_drift: regenerate in moflo.yaml, or claudeFlow.hooks.locked: true to suppress',
|
|
671
671
|
};
|
|
672
672
|
}
|
|
673
|
+
// ============================================================================
|
|
674
|
+
// 12. CLAUDE.md Injection Drift Check
|
|
675
|
+
// ============================================================================
|
|
676
|
+
/**
|
|
677
|
+
* Detect when the consumer's `<root>/CLAUDE.md` MoFlo-injected block has
|
|
678
|
+
* drifted from the canonical block the current `claudemd-generator` produces.
|
|
679
|
+
* Analogue of `Hook Block Drift` for CLAUDE.md content.
|
|
680
|
+
*
|
|
681
|
+
* The session-start launcher refreshes shipped guidance files on every
|
|
682
|
+
* version change, but the CLAUDE.md injection is only rewritten by explicit
|
|
683
|
+
* `flo init` / `flo-setup`. Without this check, consumers carry stale
|
|
684
|
+
* injection content (and stale guidance pointers) indefinitely.
|
|
685
|
+
*
|
|
686
|
+
* Five states map to four reportable statuses:
|
|
687
|
+
* no-file → warn (run `flo init`)
|
|
688
|
+
* no-marker → warn (run `flo init` / `flo-setup`)
|
|
689
|
+
* legacy-marker → warn (auto-fixable — replace legacy block)
|
|
690
|
+
* in-sync → pass
|
|
691
|
+
* drifted → warn (auto-fixable — refresh block)
|
|
692
|
+
*/
|
|
693
|
+
export async function checkClaudeMdInjectionDrift() {
|
|
694
|
+
const projectDir = findConsumerProjectDir();
|
|
695
|
+
const claudeMdPath = join(projectDir, 'CLAUDE.md');
|
|
696
|
+
// Respect `auto_update.claudemd_injection_drift: off` for consumers who
|
|
697
|
+
// explicitly opt out (mirrors the launcher's behaviour and the Hook Block
|
|
698
|
+
// Drift check). Read the config first so the off-mode skip is cheap.
|
|
699
|
+
try {
|
|
700
|
+
const { loadMofloConfig } = await import('../config/moflo-config.js');
|
|
701
|
+
const cfg = loadMofloConfig(projectDir);
|
|
702
|
+
if (cfg.auto_update.claudemd_injection_drift === 'off') {
|
|
703
|
+
return {
|
|
704
|
+
name: 'CLAUDE.md Injection Drift',
|
|
705
|
+
status: 'pass',
|
|
706
|
+
message: 'drift check skipped — auto_update.claudemd_injection_drift: off',
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
catch { /* config read failure — fall through to drift check */ }
|
|
711
|
+
if (!existsSync(claudeMdPath)) {
|
|
712
|
+
return {
|
|
713
|
+
name: 'CLAUDE.md Injection Drift',
|
|
714
|
+
status: 'warn',
|
|
715
|
+
message: 'CLAUDE.md not found',
|
|
716
|
+
fix: 'npx moflo init',
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
let contents;
|
|
720
|
+
try {
|
|
721
|
+
contents = readFileSync(claudeMdPath, 'utf-8');
|
|
722
|
+
}
|
|
723
|
+
catch (e) {
|
|
724
|
+
return {
|
|
725
|
+
name: 'CLAUDE.md Injection Drift',
|
|
726
|
+
status: 'warn',
|
|
727
|
+
message: `cannot read CLAUDE.md: ${errorDetail(e)}`,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
// Dynamic-import the generator + drift detector so the dist-vs-source
|
|
731
|
+
// path resolution stays consistent with the launcher.
|
|
732
|
+
const { generateClaudeMd } = await import('../init/claudemd-generator.js');
|
|
733
|
+
const { computeInjectionDrift } = await import('../services/claudemd-injection.js');
|
|
734
|
+
// Use `{}` (not DEFAULT_INIT_OPTIONS) to match the launcher's call —
|
|
735
|
+
// the generator ignores the argument, but matching call shape removes the
|
|
736
|
+
// possibility of a future generator change diverging the two surfaces.
|
|
737
|
+
const canonical = generateClaudeMd({});
|
|
738
|
+
const report = computeInjectionDrift(contents, canonical);
|
|
739
|
+
switch (report.state) {
|
|
740
|
+
case 'in-sync':
|
|
741
|
+
return {
|
|
742
|
+
name: 'CLAUDE.md Injection Drift',
|
|
743
|
+
status: 'pass',
|
|
744
|
+
message: 'CLAUDE.md injection block matches reference',
|
|
745
|
+
};
|
|
746
|
+
case 'no-marker':
|
|
747
|
+
return {
|
|
748
|
+
name: 'CLAUDE.md Injection Drift',
|
|
749
|
+
status: 'warn',
|
|
750
|
+
message: 'CLAUDE.md has no MOFLO:INJECTED:START block',
|
|
751
|
+
fix: 'npx flo-setup',
|
|
752
|
+
};
|
|
753
|
+
case 'legacy-marker':
|
|
754
|
+
return {
|
|
755
|
+
name: 'CLAUDE.md Injection Drift',
|
|
756
|
+
status: 'warn',
|
|
757
|
+
message: 'CLAUDE.md uses a legacy moflo marker pair (pre-MOFLO:INJECTED) — auto-fix replaces with current block',
|
|
758
|
+
fix: 'npx flo-setup --update',
|
|
759
|
+
};
|
|
760
|
+
case 'drifted':
|
|
761
|
+
return {
|
|
762
|
+
name: 'CLAUDE.md Injection Drift',
|
|
763
|
+
status: 'warn',
|
|
764
|
+
message: 'CLAUDE.md injection block has drifted from reference',
|
|
765
|
+
fix: 'npx flo-setup --update',
|
|
766
|
+
};
|
|
767
|
+
case 'no-file':
|
|
768
|
+
// Defensive — `existsSync` returned true above, so this branch is
|
|
769
|
+
// unreachable in practice. Return a sane status anyway.
|
|
770
|
+
return {
|
|
771
|
+
name: 'CLAUDE.md Injection Drift',
|
|
772
|
+
status: 'warn',
|
|
773
|
+
message: 'CLAUDE.md not found',
|
|
774
|
+
fix: 'npx moflo init',
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
}
|
|
673
778
|
//# sourceMappingURL=doctor-checks-deep.js.map
|
|
@@ -160,9 +160,29 @@ export async function autoFixCheck(check) {
|
|
|
160
160
|
return false;
|
|
161
161
|
}
|
|
162
162
|
},
|
|
163
|
+
// #1150 — SIGTERM the lock-holder BEFORE unlinking the lock. The old
|
|
164
|
+
// shape (`unlink lock; daemon start`) is the bug that produced orphan
|
|
165
|
+
// daemon accumulation: if the lock-holder PID was still alive, the
|
|
166
|
+
// unlink left it running and the respawn produced a second same-project
|
|
167
|
+
// daemon. Mirrors the 'Daemon Version Skew' / 'Daemon Identity Match'
|
|
168
|
+
// shape which got this right.
|
|
169
|
+
//
|
|
170
|
+
// Also reaps any same-project orphans whose PIDs aren't recorded in the
|
|
171
|
+
// lock — those are the daemons that survived prior buggy fixes.
|
|
163
172
|
'Daemon Status': async () => {
|
|
164
|
-
const
|
|
165
|
-
const
|
|
173
|
+
const cwd = process.cwd();
|
|
174
|
+
const { getDaemonLockPayload, reapSameProjectOrphans } = await import('../services/daemon-lock.js');
|
|
175
|
+
const payload = getDaemonLockPayload(cwd);
|
|
176
|
+
if (payload?.pid && payload.pid > 0) {
|
|
177
|
+
try {
|
|
178
|
+
process.kill(payload.pid, 'SIGTERM');
|
|
179
|
+
}
|
|
180
|
+
catch { /* already dead */ }
|
|
181
|
+
}
|
|
182
|
+
// Wipe other same-project daemons that the lock doesn't account for.
|
|
183
|
+
reapSameProjectOrphans(cwd);
|
|
184
|
+
const lockFile = join(cwd, '.moflo', 'daemon.lock');
|
|
185
|
+
const pidFile = join(cwd, '.moflo', 'daemon.pid');
|
|
166
186
|
try {
|
|
167
187
|
if (existsSync(lockFile))
|
|
168
188
|
unlinkSync(lockFile);
|
|
@@ -195,6 +215,59 @@ export async function autoFixCheck(check) {
|
|
|
195
215
|
catch { /* ok */ }
|
|
196
216
|
return runFixCommand('npx moflo daemon start');
|
|
197
217
|
},
|
|
218
|
+
// #1150 — terminate same-project orphan daemons. Keep the lock-holder
|
|
219
|
+
// alive if it shows up in the scan (it's the canonical daemon). If the
|
|
220
|
+
// lock-holder is missing/stale, kill all candidates and let the next
|
|
221
|
+
// session-start respawn a clean one. The pre-computed `pids` list is
|
|
222
|
+
// threaded into `reapSameProjectOrphans` so we don't re-run the
|
|
223
|
+
// OS process scan inside it.
|
|
224
|
+
'Daemon Orphan': async () => {
|
|
225
|
+
const cwd = process.cwd();
|
|
226
|
+
const { findProjectDaemonPids, getDaemonLockHolder, reapSameProjectOrphans } = await import('../services/daemon-lock.js');
|
|
227
|
+
const pids = findProjectDaemonPids(cwd);
|
|
228
|
+
if (pids.length <= 1)
|
|
229
|
+
return true; // already healthy
|
|
230
|
+
const lockHolder = getDaemonLockHolder(cwd);
|
|
231
|
+
if (lockHolder != null && pids.includes(lockHolder)) {
|
|
232
|
+
const { survived } = reapSameProjectOrphans(cwd, process.pid, lockHolder, pids);
|
|
233
|
+
return survived.length === 0;
|
|
234
|
+
}
|
|
235
|
+
// No identifiable canonical daemon — kill them all, clear the lock,
|
|
236
|
+
// respawn fresh.
|
|
237
|
+
const { survived } = reapSameProjectOrphans(cwd, process.pid, undefined, pids);
|
|
238
|
+
const lockFile = join(cwd, '.moflo', 'daemon.lock');
|
|
239
|
+
try {
|
|
240
|
+
if (existsSync(lockFile))
|
|
241
|
+
unlinkSync(lockFile);
|
|
242
|
+
}
|
|
243
|
+
catch { /* ok */ }
|
|
244
|
+
if (survived.length > 0)
|
|
245
|
+
return false;
|
|
246
|
+
return runFixCommand('npx moflo daemon start');
|
|
247
|
+
},
|
|
248
|
+
// #1145 — daemon claims a different projectRoot than ours (or has no
|
|
249
|
+
// port in its lock so we can't verify). Same recycle pattern as version
|
|
250
|
+
// skew: SIGTERM the local daemon, clear the lock, respawn. Then the new
|
|
251
|
+
// daemon binds the per-project deterministic port and stamps it into
|
|
252
|
+
// the lock — clients can discover it without guessing.
|
|
253
|
+
'Daemon Identity Match': async () => {
|
|
254
|
+
const cwd = process.cwd();
|
|
255
|
+
const { getDaemonLockPayload } = await import('../services/daemon-lock.js');
|
|
256
|
+
const payload = getDaemonLockPayload(cwd);
|
|
257
|
+
if (payload?.pid && payload.pid > 0) {
|
|
258
|
+
try {
|
|
259
|
+
process.kill(payload.pid, 'SIGTERM');
|
|
260
|
+
}
|
|
261
|
+
catch { /* already dead */ }
|
|
262
|
+
}
|
|
263
|
+
const lockFile = join(cwd, '.moflo', 'daemon.lock');
|
|
264
|
+
try {
|
|
265
|
+
if (existsSync(lockFile))
|
|
266
|
+
unlinkSync(lockFile);
|
|
267
|
+
}
|
|
268
|
+
catch { /* ok */ }
|
|
269
|
+
return runFixCommand('npx moflo daemon start');
|
|
270
|
+
},
|
|
198
271
|
'Embedding Coverage Truth': async () => {
|
|
199
272
|
// Same as the existing Embeddings fix — rebuild the cache by re-running
|
|
200
273
|
// the embeddings pipeline. Routes through `npx moflo` so the consumer
|
|
@@ -255,6 +328,30 @@ export async function autoFixCheck(check) {
|
|
|
255
328
|
'Gate Health': async () => {
|
|
256
329
|
return fixGateHealthHooks();
|
|
257
330
|
},
|
|
331
|
+
// Refresh the consumer's CLAUDE.md MoFlo block in place using the
|
|
332
|
+
// shared `applyInjectionReplacement` service. Idempotent: a re-run sees
|
|
333
|
+
// `state === 'in-sync'` and the autoFix dispatcher skips this entry.
|
|
334
|
+
'CLAUDE.md Injection Drift': async () => {
|
|
335
|
+
const projectRoot = findProjectRoot();
|
|
336
|
+
const claudeMdPath = join(projectRoot, 'CLAUDE.md');
|
|
337
|
+
try {
|
|
338
|
+
const { generateClaudeMd } = await import('../init/claudemd-generator.js');
|
|
339
|
+
const { applyInjectionReplacement } = await import('../services/claudemd-injection.js');
|
|
340
|
+
const canonical = generateClaudeMd({});
|
|
341
|
+
const existing = existsSync(claudeMdPath) ? readFileSync(claudeMdPath, 'utf-8') : null;
|
|
342
|
+
const result = applyInjectionReplacement(existing, canonical);
|
|
343
|
+
if (!result.changed || !result.contents)
|
|
344
|
+
return false;
|
|
345
|
+
// atomicWriteFileSync guards against a concurrent reader (Claude Code
|
|
346
|
+
// re-scanning CLAUDE.md mid-fix) seeing a truncated file.
|
|
347
|
+
atomicWriteFileSync(claudeMdPath, result.contents);
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
catch (e) {
|
|
351
|
+
output.writeln(output.warning(` CLAUDE.md repair failed: ${errorDetail(e)}`));
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
},
|
|
258
355
|
'Embedding hygiene': async () => {
|
|
259
356
|
// The session-start launcher already runs the same migration BEFORE
|
|
260
357
|
// daemon/MCP boot — that's where consumer autoheal happens. Running
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Kept separate from `doctor.ts` so the orchestration file stays small and the
|
|
5
5
|
* registry can be inspected/extended without re-touching command-action code.
|
|
6
6
|
*/
|
|
7
|
-
import { checkSubagentHealth, checkSpellExecution, checkMcpToolInvocation, checkHookExecution, checkMcpSpellIntegration, checkGateHealth, checkHookBlockDrift, checkMofloDbBridge, } from './doctor-checks-deep.js';
|
|
7
|
+
import { checkSubagentHealth, checkSpellExecution, checkMcpToolInvocation, checkHookExecution, checkMcpSpellIntegration, checkGateHealth, checkHookBlockDrift, checkClaudeMdInjectionDrift, checkMofloDbBridge, } from './doctor-checks-deep.js';
|
|
8
8
|
import { checkEmbeddingHygiene } from './doctor-embedding-hygiene.js';
|
|
9
9
|
import { checkDaemonVersionSkew } from './doctor-checks-version-skew.js';
|
|
10
10
|
import { checkEmbeddingCoverageTruth } from './doctor-checks-coverage-truth.js';
|
|
@@ -12,7 +12,7 @@ import { checkWritersAudit } from './doctor-checks-writers-audit.js';
|
|
|
12
12
|
import { checkSwarmFunctional, checkHiveMindFunctional, } from './doctor-checks-swarm.js';
|
|
13
13
|
import { checkMemoryAccessFunctional } from './doctor-checks-memory-access.js';
|
|
14
14
|
import { checkBuildTools, checkClaudeCode, checkDiskSpace, checkGit, checkGitRepo, checkNodeVersion, checkNpmVersion, } from './doctor-checks-runtime.js';
|
|
15
|
-
import { checkConfigFile, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMofloYamlCompliance, checkStatusLine, checkTestDirs, } from './doctor-checks-config.js';
|
|
15
|
+
import { checkConfigFile, checkDaemonIdentity, checkDaemonOrphan, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMofloYamlCompliance, checkStatusLine, checkTestDirs, } from './doctor-checks-config.js';
|
|
16
16
|
import { checkSpellEngine, checkSandboxTier } from './doctor-checks-platform.js';
|
|
17
17
|
import { checkEmbeddings, checkSemanticQuality, } from './doctor-checks-memory.js';
|
|
18
18
|
import { checkIntelligence } from './doctor-checks-intelligence.js';
|
|
@@ -37,6 +37,8 @@ export const allChecks = [
|
|
|
37
37
|
checkStatusLine,
|
|
38
38
|
checkDaemonStatus,
|
|
39
39
|
checkDaemonVersionSkew,
|
|
40
|
+
checkDaemonIdentity,
|
|
41
|
+
checkDaemonOrphan,
|
|
40
42
|
checkDaemonWriteRouting,
|
|
41
43
|
checkWritersAudit,
|
|
42
44
|
checkMemoryDatabase,
|
|
@@ -63,6 +65,7 @@ export const allChecks = [
|
|
|
63
65
|
checkHookExecution,
|
|
64
66
|
checkGateHealth,
|
|
65
67
|
checkHookBlockDrift,
|
|
68
|
+
checkClaudeMdInjectionDrift,
|
|
66
69
|
checkMofloDbBridge,
|
|
67
70
|
// Issue #818 / epic #798 — coordinator-path tripwires. They share the
|
|
68
71
|
// singleton coordinator with checkSubagentHealth above and assert by
|
|
@@ -94,6 +97,13 @@ export const componentMap = {
|
|
|
94
97
|
'skew': checkDaemonVersionSkew,
|
|
95
98
|
'daemon-write-routing': checkDaemonWriteRouting,
|
|
96
99
|
'write-routing': checkDaemonWriteRouting,
|
|
100
|
+
'daemon-identity': checkDaemonIdentity,
|
|
101
|
+
'daemon-identity-match': checkDaemonIdentity,
|
|
102
|
+
'identity': checkDaemonIdentity,
|
|
103
|
+
'daemon-orphan': checkDaemonOrphan,
|
|
104
|
+
'daemon-orphans': checkDaemonOrphan,
|
|
105
|
+
'orphan': checkDaemonOrphan,
|
|
106
|
+
'orphans': checkDaemonOrphan,
|
|
97
107
|
'writers-audit': checkWritersAudit,
|
|
98
108
|
'writers': checkWritersAudit,
|
|
99
109
|
'memory': checkMemoryDatabase,
|
|
@@ -127,6 +137,9 @@ export const componentMap = {
|
|
|
127
137
|
'gate': checkGateHealth,
|
|
128
138
|
'hook-drift': checkHookBlockDrift,
|
|
129
139
|
'drift': checkHookBlockDrift,
|
|
140
|
+
'claudemd-drift': checkClaudeMdInjectionDrift,
|
|
141
|
+
'claudemd': checkClaudeMdInjectionDrift,
|
|
142
|
+
'injection-drift': checkClaudeMdInjectionDrift,
|
|
130
143
|
'sandbox': checkSandboxTier,
|
|
131
144
|
'sandbox-tier': checkSandboxTier,
|
|
132
145
|
'moflodb': checkMofloDbBridge,
|
|
@@ -103,7 +103,7 @@ const storeCommand = {
|
|
|
103
103
|
size: Buffer.byteLength(value, 'utf8')
|
|
104
104
|
};
|
|
105
105
|
output.printInfo(`Storing in ${namespace}/${key}...`);
|
|
106
|
-
// Use direct
|
|
106
|
+
// Use direct memory-backend storage with automatic embedding generation
|
|
107
107
|
try {
|
|
108
108
|
const { storeEntry } = await import('../memory/memory-initializer.js');
|
|
109
109
|
if (asVector) {
|
|
@@ -175,7 +175,7 @@ const retrieveCommand = {
|
|
|
175
175
|
output.printError('Key is required');
|
|
176
176
|
return { success: false, exitCode: 1 };
|
|
177
177
|
}
|
|
178
|
-
// Use
|
|
178
|
+
// Use the memory backend directly for consistent data access
|
|
179
179
|
try {
|
|
180
180
|
const { getEntry } = await import('../memory/memory-initializer.js');
|
|
181
181
|
const result = await getEntry({ key, namespace });
|
|
@@ -302,7 +302,7 @@ const searchCommand = {
|
|
|
302
302
|
}
|
|
303
303
|
output.printInfo(`Searching: "${query}" (${searchType})`);
|
|
304
304
|
output.writeln();
|
|
305
|
-
// Use direct
|
|
305
|
+
// Use direct memory-backend search with vector similarity
|
|
306
306
|
try {
|
|
307
307
|
const { searchEntries } = await import('../memory/memory-initializer.js');
|
|
308
308
|
const searchResult = await searchEntries({
|
|
@@ -381,7 +381,7 @@ const listCommand = {
|
|
|
381
381
|
action: async (ctx) => {
|
|
382
382
|
const namespace = ctx.flags.namespace;
|
|
383
383
|
const limit = ctx.flags.limit;
|
|
384
|
-
// Use
|
|
384
|
+
// Use the memory backend directly for consistent data access
|
|
385
385
|
try {
|
|
386
386
|
const { listEntries } = await import('../memory/memory-initializer.js');
|
|
387
387
|
const listResult = await listEntries({ namespace, limit, offset: 0 });
|
|
@@ -499,7 +499,7 @@ const deleteCommand = {
|
|
|
499
499
|
return { success: true };
|
|
500
500
|
}
|
|
501
501
|
}
|
|
502
|
-
// Use
|
|
502
|
+
// Use the memory backend directly for consistent data access (Issue #980)
|
|
503
503
|
try {
|
|
504
504
|
const { deleteEntry } = await import('../memory/memory-initializer.js');
|
|
505
505
|
const result = await deleteEntry({ key, namespace });
|
|
@@ -1034,10 +1034,10 @@ const importCommand = {
|
|
|
1034
1034
|
}
|
|
1035
1035
|
}
|
|
1036
1036
|
};
|
|
1037
|
-
// Init subcommand - initialize memory database using
|
|
1037
|
+
// Init subcommand - initialize memory database using node:sqlite
|
|
1038
1038
|
const initMemoryCommand = {
|
|
1039
1039
|
name: 'init',
|
|
1040
|
-
description: 'Initialize memory database with
|
|
1040
|
+
description: 'Initialize memory database with node:sqlite (Node 22+ built-in) - includes vector embeddings, pattern learning, temporal decay',
|
|
1041
1041
|
options: [
|
|
1042
1042
|
{
|
|
1043
1043
|
name: 'backend',
|
|
@@ -2637,7 +2637,7 @@ export const memoryCommand = {
|
|
|
2637
2637
|
output.writeln();
|
|
2638
2638
|
output.writeln('Subcommands:');
|
|
2639
2639
|
output.printList([
|
|
2640
|
-
`${output.highlight('init')} - Initialize memory database (
|
|
2640
|
+
`${output.highlight('init')} - Initialize memory database (node:sqlite)`,
|
|
2641
2641
|
`${output.highlight('store')} - Store data in memory`,
|
|
2642
2642
|
`${output.highlight('retrieve')} - Retrieve data from memory`,
|
|
2643
2643
|
`${output.highlight('search')} - Semantic/vector search`,
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Created with ❤️ by motailz.com
|
|
6
6
|
*/
|
|
7
7
|
import { output } from '../output.js';
|
|
8
|
+
import { findProjectRoot } from '../services/project-root.js';
|
|
9
|
+
import { MOFLO_DIR } from '../services/moflo-paths.js';
|
|
8
10
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
9
11
|
// Train subcommand - REAL WASM training with MoVector
|
|
10
12
|
const trainCommand = {
|
|
@@ -673,8 +675,8 @@ const optimizeCommand = {
|
|
|
673
675
|
await initializeIntelligence();
|
|
674
676
|
const patterns = await getAllPatterns();
|
|
675
677
|
const stats = getIntelligenceStats();
|
|
676
|
-
// Get actual pattern storage size
|
|
677
|
-
const patternDir = path.join(
|
|
678
|
+
// Get actual pattern storage size (#1152: anchor on projectRoot, not cwd)
|
|
679
|
+
const patternDir = path.join(findProjectRoot(), MOFLO_DIR, 'neural');
|
|
678
680
|
let beforeSize = 0;
|
|
679
681
|
try {
|
|
680
682
|
const patternFile = path.join(patternDir, 'patterns.json');
|
|
@@ -849,8 +851,8 @@ const exportCommand = {
|
|
|
849
851
|
totalUsage: 0,
|
|
850
852
|
},
|
|
851
853
|
};
|
|
852
|
-
// Load patterns from local storage
|
|
853
|
-
const memoryDir = path.join(
|
|
854
|
+
// Load patterns from local storage (#1152: projectRoot-anchored)
|
|
855
|
+
const memoryDir = path.join(findProjectRoot(), MOFLO_DIR, 'memory');
|
|
854
856
|
const patternsFile = path.join(memoryDir, 'patterns.json');
|
|
855
857
|
if (fs.existsSync(patternsFile)) {
|
|
856
858
|
const patterns = JSON.parse(fs.readFileSync(patternsFile, 'utf8'));
|
|
@@ -1242,8 +1244,8 @@ const importCommand = {
|
|
|
1242
1244
|
if (validPatterns.length < patterns.length) {
|
|
1243
1245
|
output.writeln(output.warning(`Filtered ${patterns.length - validPatterns.length} suspicious patterns`));
|
|
1244
1246
|
}
|
|
1245
|
-
// Save to local memory
|
|
1246
|
-
const memoryDir = path.join(
|
|
1247
|
+
// Save to local memory (#1152: projectRoot-anchored)
|
|
1248
|
+
const memoryDir = path.join(findProjectRoot(), MOFLO_DIR, 'memory');
|
|
1247
1249
|
if (!fs.existsSync(memoryDir)) {
|
|
1248
1250
|
fs.mkdirSync(memoryDir, { recursive: true });
|
|
1249
1251
|
}
|