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.
Files changed (38) hide show
  1. package/.claude/guidance/shipped/moflo-cli-reference.md +1 -1
  2. package/.claude/guidance/shipped/moflo-memory-strategy.md +1 -1
  3. package/.claude/guidance/shipped/moflo-yaml-reference.md +19 -5
  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/bin/session-start-launcher.mjs +189 -15
  10. package/bin/setup-project.mjs +38 -58
  11. package/dist/src/cli/commands/daemon.js +31 -10
  12. package/dist/src/cli/commands/doctor-checks-config.js +139 -1
  13. package/dist/src/cli/commands/doctor-checks-deep.js +105 -0
  14. package/dist/src/cli/commands/doctor-fixes.js +99 -2
  15. package/dist/src/cli/commands/doctor-registry.js +15 -2
  16. package/dist/src/cli/commands/memory.js +8 -8
  17. package/dist/src/cli/commands/neural.js +8 -6
  18. package/dist/src/cli/config/moflo-config.js +79 -3
  19. package/dist/src/cli/index.js +18 -19
  20. package/dist/src/cli/init/claudemd-generator.js +6 -2
  21. package/dist/src/cli/init/moflo-init.js +13 -21
  22. package/dist/src/cli/init/moflo-yaml-template.js +1 -1
  23. package/dist/src/cli/mcp-server.js +59 -10
  24. package/dist/src/cli/mcp-tools/memory-tools.js +46 -27
  25. package/dist/src/cli/memory/auto-memory-bridge.js +1 -1
  26. package/dist/src/cli/memory/controllers/attestation-log.js +1 -1
  27. package/dist/src/cli/memory/controllers/causal-graph.js +1 -1
  28. package/dist/src/cli/memory/daemon-write-client.js +178 -49
  29. package/dist/src/cli/memory/database-provider.js +58 -3
  30. package/dist/src/cli/memory/intelligence.js +54 -26
  31. package/dist/src/cli/memory/memory-initializer.js +21 -11
  32. package/dist/src/cli/services/claudemd-injection.js +173 -0
  33. package/dist/src/cli/services/daemon-dashboard.js +94 -25
  34. package/dist/src/cli/services/daemon-lock.js +390 -3
  35. package/dist/src/cli/services/daemon-port.js +217 -0
  36. package/dist/src/cli/version.js +1 -1
  37. package/package.json +2 -2
  38. package/dist/src/cli/config-adapter.js +0 -182
@@ -32,13 +32,13 @@ import { errorDetail } from '../shared/utils/error-detail.js';
32
32
  export function resolveDashboardPort(flagValue, envValue) {
33
33
  const source = flagValue ?? envValue;
34
34
  if (!source)
35
- return { ok: true, port: DEFAULT_DASHBOARD_PORT };
35
+ return { ok: true, port: DEFAULT_DASHBOARD_PORT, explicit: false };
36
36
  const parsed = parseInt(source, 10);
37
37
  if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
38
38
  const label = flagValue ? 'dashboard port' : 'MOFLO_DAEMON_PORT';
39
39
  return { ok: false, error: `Invalid ${label}: ${source} (must be 1-65535)` };
40
40
  }
41
- return { ok: true, port: parsed };
41
+ return { ok: true, port: parsed, explicit: true };
42
42
  }
43
43
  // Start daemon subcommand
44
44
  const startCommand = {
@@ -76,6 +76,7 @@ const startCommand = {
76
76
  return { success: false, exitCode: 1 };
77
77
  }
78
78
  const dashboardPort = portResult.port;
79
+ const dashboardPortExplicit = portResult.explicit;
79
80
  // Parse resource threshold overrides from CLI flags
80
81
  const config = {};
81
82
  const rawMaxCpu = ctx.flags.maxCpuLoad;
@@ -109,7 +110,7 @@ const startCommand = {
109
110
  }
110
111
  // Background mode (default): fork a detached process
111
112
  if (!foreground) {
112
- return startBackgroundDaemon(projectRoot, quiet, rawMaxCpu, rawMinMem, dashboardPort, noDashboard);
113
+ return startBackgroundDaemon(projectRoot, quiet, rawMaxCpu, rawMinMem, dashboardPortExplicit ? dashboardPort : undefined, noDashboard);
113
114
  }
114
115
  // Foreground mode: run in current process (blocks terminal)
115
116
  try {
@@ -151,7 +152,7 @@ const startCommand = {
151
152
  const status = daemon.getStatus();
152
153
  spinner.succeed('Worker daemon started (foreground mode)');
153
154
  const { dashboard } = await attachDaemonServices(daemon, {
154
- projectRoot, noDashboard, dashboardPort, verbose: true,
155
+ projectRoot, noDashboard, dashboardPort, dashboardPortExplicit, verbose: true,
155
156
  });
156
157
  output.writeln();
157
158
  output.printBox([
@@ -207,7 +208,7 @@ const startCommand = {
207
208
  }
208
209
  else {
209
210
  const daemon = await startDaemon(projectRoot, config);
210
- await attachDaemonServices(daemon, { projectRoot, noDashboard, dashboardPort, verbose: false });
211
+ await attachDaemonServices(daemon, { projectRoot, noDashboard, dashboardPort, dashboardPortExplicit, verbose: false });
211
212
  await new Promise(() => { }); // Keep alive
212
213
  }
213
214
  return { success: true };
@@ -243,15 +244,25 @@ async function attachDaemonServices(daemon, opts) {
243
244
  if (!opts.noDashboard) {
244
245
  try {
245
246
  dashboard = await startDashboard(daemon, {
246
- port: opts.dashboardPort,
247
+ // Pass the resolved port only when the caller explicitly pinned it
248
+ // (CLI flag / env). Otherwise let startDashboard pick the
249
+ // deterministic per-project port via serverPortCandidates (#1145).
250
+ port: opts.dashboardPortExplicit ? opts.dashboardPort : undefined,
247
251
  memory,
248
252
  schedulerEnabledInConfig: schedulerConfig.enabled,
253
+ projectRoot: opts.projectRoot,
249
254
  });
250
255
  if (opts.verbose)
251
256
  output.printSuccess(`The Luminarium: http://localhost:${dashboard.port}`);
252
257
  }
253
258
  catch (err) {
254
- logWarn(`The Luminarium failed to start: ${errorDetail(err)}`);
259
+ // #1145 §9.4 hard-fail on bind exhaustion. Pre-#1145 we swallowed
260
+ // this; the daemon stayed alive doing worker-only work while clients
261
+ // routed to whichever daemon happened to be on the legacy default
262
+ // port. Re-throw so the spawn launcher sees a non-zero exit and the
263
+ // healer flags the failure instead of silently continuing.
264
+ logWarn(`The Luminarium failed to bind — daemon will exit so clients don't silently route to a foreign daemon: ${errorDetail(err)}`);
265
+ throw err;
255
266
  }
256
267
  }
257
268
  if (!schedulerConfig.enabled) {
@@ -350,11 +361,14 @@ async function startBackgroundDaemon(projectRoot, quiet, maxCpuLoad, minFreeMemo
350
361
  if (minFreeMemory && SPAWN_NUMERIC_RE.test(minFreeMemory)) {
351
362
  spawnArgs.push('--min-free-memory', minFreeMemory);
352
363
  }
353
- // Forward dashboard flags
364
+ // Forward dashboard flags. With #1145 the foreground child resolves its
365
+ // own deterministic per-project port when no `--dashboard-port` flag is
366
+ // passed — only forward the flag when the caller explicitly pinned it
367
+ // (signaled by `dashboardPort` being defined here).
354
368
  if (noDashboard) {
355
369
  spawnArgs.push('--no-dashboard');
356
370
  }
357
- else if (dashboardPort && dashboardPort !== DEFAULT_DASHBOARD_PORT) {
371
+ else if (dashboardPort != null) {
358
372
  spawnArgs.push('--dashboard-port', String(dashboardPort));
359
373
  }
360
374
  const daemonEnv = {
@@ -413,7 +427,14 @@ async function startBackgroundDaemon(projectRoot, quiet, maxCpuLoad, minFreeMemo
413
427
  if (!quiet) {
414
428
  output.printSuccess(`Daemon started in background (PID: ${pid})`);
415
429
  if (!noDashboard) {
416
- output.printInfo(`The Luminarium: http://localhost:${dashboardPort ?? DEFAULT_DASHBOARD_PORT}`);
430
+ if (dashboardPort != null) {
431
+ output.printInfo(`The Luminarium: http://localhost:${dashboardPort}`);
432
+ }
433
+ else {
434
+ // #1145 — port is project-deterministic and assigned at bind time;
435
+ // read it from .moflo/daemon.lock once the child finishes startup.
436
+ output.printInfo(`The Luminarium: see http://localhost:<port from .moflo/daemon.lock>`);
437
+ }
417
438
  }
418
439
  output.printInfo(`Logs: ${logFile}`);
419
440
  output.printInfo(`Stop with: claude-flow daemon stop`);
@@ -6,7 +6,8 @@
6
6
  import { existsSync, readFileSync, statSync } from 'fs';
7
7
  import { join } from 'path';
8
8
  import os from 'os';
9
- import { getDaemonLockHolder } from '../services/daemon-lock.js';
9
+ import { findProjectDaemonPids, getDaemonLockHolder, getDaemonLockPayload, } from '../services/daemon-lock.js';
10
+ import { resolveClientPort, LEGACY_DEFAULT_PORT, probeDaemonHealth as probeDaemonHealthIdentity, normalizeProjectRoot, } from '../services/daemon-port.js';
10
11
  import { legacyMemoryDbPath, memoryDbCandidatePaths, memoryDbPath, } from '../services/moflo-paths.js';
11
12
  import { probeDbIntegrity } from '../services/memory-db-integrity-repair.js';
12
13
  import { findProjectRoot } from '../services/project-root.js';
@@ -102,6 +103,143 @@ export async function checkDaemonStatus() {
102
103
  return { name: 'Daemon Status', status: 'warn', message: `Unable to check: ${errorDetail(e)}`, fix: 'claude-flow daemon status' };
103
104
  }
104
105
  }
106
+ /**
107
+ * Daemon identity check (#1145).
108
+ *
109
+ * Reads `.moflo/daemon.lock` → probes `/api/health` on the recorded port →
110
+ * confirms the daemon's reported `projectRoot` matches `findProjectRoot()`.
111
+ * Catches the silent cross-project routing class where two moflo daemons
112
+ * (e.g. moflo-dev + a consumer) share a port and the client hits the wrong
113
+ * one.
114
+ *
115
+ * Status semantics:
116
+ * - `pass` — no daemon (cleanly absent) OR identity matches.
117
+ * - `warn` — daemon has no port in lock (pre-#1145 daemon; harmless if
118
+ * no collision, but should be recycled to upgrade).
119
+ * - `fail` — `/api/health` reports a different project. Routing is
120
+ * polluted; the healer fix kills the foreign-identity daemon and
121
+ * respawns the local one.
122
+ */
123
+ export async function checkDaemonIdentity(cwd = process.cwd()) {
124
+ const projectRoot = findProjectRoot({ cwd });
125
+ const payload = getDaemonLockPayload(projectRoot);
126
+ if (!payload) {
127
+ return {
128
+ name: 'Daemon Identity Match',
129
+ status: 'pass',
130
+ message: 'No daemon running (nothing to verify)',
131
+ };
132
+ }
133
+ // Lock present but no port field — pre-#1145 daemon. Identity check is
134
+ // best-effort; surface as warn so the user knows to recycle but don't
135
+ // hard-fail unless we can prove cross-project routing.
136
+ const portFromLock = payload.port;
137
+ const probePort = portFromLock ?? resolveClientPort(projectRoot);
138
+ if (!portFromLock) {
139
+ // Try the legacy default explicitly so consumers running an
140
+ // un-upgraded daemon still get a useful diagnostic.
141
+ const probe = await probeDaemonHealthIdentity(LEGACY_DEFAULT_PORT, 1500);
142
+ if (probe.kind === 'identity'
143
+ && normalizeProjectRoot(probe.projectRoot) !== normalizeProjectRoot(projectRoot)) {
144
+ return {
145
+ name: 'Daemon Identity Match',
146
+ status: 'fail',
147
+ message: `Daemon at 127.0.0.1:${LEGACY_DEFAULT_PORT} claims project '${probe.projectRoot}' — cross-project routing active`,
148
+ fix: 'flo healer --fix -c daemon-identity',
149
+ };
150
+ }
151
+ return {
152
+ name: 'Daemon Identity Match',
153
+ status: 'warn',
154
+ message: 'Daemon lock has no port field — pre-#1145 daemon; recycle to enable port discovery',
155
+ fix: 'flo healer --fix -c daemon-identity',
156
+ };
157
+ }
158
+ const probe = await probeDaemonHealthIdentity(probePort, 1500);
159
+ if (probe.kind === 'unreachable') {
160
+ return {
161
+ name: 'Daemon Identity Match',
162
+ status: 'warn',
163
+ message: `Daemon at 127.0.0.1:${probePort} unreachable on /api/health`,
164
+ fix: 'flo healer --fix -c daemon-identity',
165
+ };
166
+ }
167
+ if (probe.kind === 'legacy') {
168
+ return {
169
+ name: 'Daemon Identity Match',
170
+ status: 'warn',
171
+ message: `Daemon at 127.0.0.1:${probePort} has no /api/health (legacy) — recycle to upgrade`,
172
+ fix: 'flo healer --fix -c daemon-identity',
173
+ };
174
+ }
175
+ if (normalizeProjectRoot(probe.projectRoot) !== normalizeProjectRoot(projectRoot)) {
176
+ return {
177
+ name: 'Daemon Identity Match',
178
+ status: 'fail',
179
+ message: `Daemon at 127.0.0.1:${probePort} claims project '${probe.projectRoot}' but cwd is '${projectRoot}'`,
180
+ fix: 'flo healer --fix -c daemon-identity',
181
+ };
182
+ }
183
+ return {
184
+ name: 'Daemon Identity Match',
185
+ status: 'pass',
186
+ message: `OK (port ${probePort}, pid ${payload.pid})`,
187
+ };
188
+ }
189
+ /**
190
+ * Same-project orphan daemon check (#1150).
191
+ *
192
+ * Counts moflo daemon processes whose command line is rooted at THIS
193
+ * project's CLI binary. Healthy state: 0 (no daemon) or 1 (the lock-holder).
194
+ * Failure state: >1 — multiple daemons are racing for the indexer lock and
195
+ * writing to the same `daemon-state.json`.
196
+ *
197
+ * Distinct from `Daemon Identity Match`, which catches a DIFFERENT project's
198
+ * daemon answering on a port. This check catches multiple SAME-project
199
+ * daemons sharing the project root but with one of them holding a stale or
200
+ * unlinked lock (the orphan path).
201
+ *
202
+ * Status semantics:
203
+ * - `pass` — 0 or 1 same-project daemon.
204
+ * - `fail` — 2+ same-project daemons. Auto-fix terminates all but the
205
+ * lock-recorded PID (or all + respawn if no lock matches a live one).
206
+ * The "no live lock-holder" sub-case stays `fail` rather than `warn`:
207
+ * a stale lock alongside live orphan daemons is a strictly worse state
208
+ * than an orphan that the lock knows about, not a softer one.
209
+ * - `warn` — the OS process scan itself failed (platform introspection
210
+ * unavailable). The healer is offered as a fallback but isn't binding.
211
+ */
212
+ export async function checkDaemonOrphan(cwd = process.cwd()) {
213
+ const projectRoot = findProjectRoot({ cwd });
214
+ let pids;
215
+ try {
216
+ pids = findProjectDaemonPids(projectRoot);
217
+ }
218
+ catch (e) {
219
+ return {
220
+ name: 'Daemon Orphan',
221
+ status: 'warn',
222
+ message: `Process scan failed: ${errorDetail(e)}`,
223
+ };
224
+ }
225
+ if (pids.length === 0) {
226
+ return { name: 'Daemon Orphan', status: 'pass', message: 'No daemon running (nothing to verify)' };
227
+ }
228
+ if (pids.length === 1) {
229
+ return { name: 'Daemon Orphan', status: 'pass', message: `1 daemon (pid ${pids[0]})` };
230
+ }
231
+ const lockHolder = getDaemonLockHolder(projectRoot);
232
+ const lockHolderInPids = lockHolder != null && pids.includes(lockHolder);
233
+ const orphanPids = lockHolderInPids ? pids.filter(p => p !== lockHolder) : pids;
234
+ return {
235
+ name: 'Daemon Orphan',
236
+ status: 'fail',
237
+ message: lockHolderInPids
238
+ ? `${pids.length} daemons for this project; lock holds pid ${lockHolder}, orphans: ${orphanPids.join(', ')}`
239
+ : `${pids.length} daemons for this project; no lock-holder identifiable, candidates: ${pids.join(', ')}`,
240
+ fix: 'flo healer --fix -c daemon-orphan',
241
+ };
242
+ }
105
243
  export async function checkMemoryDatabase() {
106
244
  const root = process.cwd();
107
245
  const canonical = memoryDbPath(root);
@@ -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 lockFile = join(process.cwd(), '.moflo', 'daemon.lock');
165
- const pidFile = join(process.cwd(), '.moflo', 'daemon.pid');
173
+ const cwd = process.cwd();
174
+ const { getDaemonLockPayload, reapSameProjectOrphans } = await import('../services/daemon-lock.js');
175
+ const payload = getDaemonLockPayload(cwd);
176
+ if (payload?.pid && payload.pid > 0) {
177
+ try {
178
+ process.kill(payload.pid, 'SIGTERM');
179
+ }
180
+ catch { /* already dead */ }
181
+ }
182
+ // Wipe other same-project daemons that the lock doesn't account for.
183
+ reapSameProjectOrphans(cwd);
184
+ const lockFile = join(cwd, '.moflo', 'daemon.lock');
185
+ const pidFile = join(cwd, '.moflo', 'daemon.pid');
166
186
  try {
167
187
  if (existsSync(lockFile))
168
188
  unlinkSync(lockFile);
@@ -195,6 +215,59 @@ export async function autoFixCheck(check) {
195
215
  catch { /* ok */ }
196
216
  return runFixCommand('npx moflo daemon start');
197
217
  },
218
+ // #1150 — terminate same-project orphan daemons. Keep the lock-holder
219
+ // alive if it shows up in the scan (it's the canonical daemon). If the
220
+ // lock-holder is missing/stale, kill all candidates and let the next
221
+ // session-start respawn a clean one. The pre-computed `pids` list is
222
+ // threaded into `reapSameProjectOrphans` so we don't re-run the
223
+ // OS process scan inside it.
224
+ 'Daemon Orphan': async () => {
225
+ const cwd = process.cwd();
226
+ const { findProjectDaemonPids, getDaemonLockHolder, reapSameProjectOrphans } = await import('../services/daemon-lock.js');
227
+ const pids = findProjectDaemonPids(cwd);
228
+ if (pids.length <= 1)
229
+ return true; // already healthy
230
+ const lockHolder = getDaemonLockHolder(cwd);
231
+ if (lockHolder != null && pids.includes(lockHolder)) {
232
+ const { survived } = reapSameProjectOrphans(cwd, process.pid, lockHolder, pids);
233
+ return survived.length === 0;
234
+ }
235
+ // No identifiable canonical daemon — kill them all, clear the lock,
236
+ // respawn fresh.
237
+ const { survived } = reapSameProjectOrphans(cwd, process.pid, undefined, pids);
238
+ const lockFile = join(cwd, '.moflo', 'daemon.lock');
239
+ try {
240
+ if (existsSync(lockFile))
241
+ unlinkSync(lockFile);
242
+ }
243
+ catch { /* ok */ }
244
+ if (survived.length > 0)
245
+ return false;
246
+ return runFixCommand('npx moflo daemon start');
247
+ },
248
+ // #1145 — daemon claims a different projectRoot than ours (or has no
249
+ // port in its lock so we can't verify). Same recycle pattern as version
250
+ // skew: SIGTERM the local daemon, clear the lock, respawn. Then the new
251
+ // daemon binds the per-project deterministic port and stamps it into
252
+ // the lock — clients can discover it without guessing.
253
+ 'Daemon Identity Match': async () => {
254
+ const cwd = process.cwd();
255
+ const { getDaemonLockPayload } = await import('../services/daemon-lock.js');
256
+ const payload = getDaemonLockPayload(cwd);
257
+ if (payload?.pid && payload.pid > 0) {
258
+ try {
259
+ process.kill(payload.pid, 'SIGTERM');
260
+ }
261
+ catch { /* already dead */ }
262
+ }
263
+ const lockFile = join(cwd, '.moflo', 'daemon.lock');
264
+ try {
265
+ if (existsSync(lockFile))
266
+ unlinkSync(lockFile);
267
+ }
268
+ catch { /* ok */ }
269
+ return runFixCommand('npx moflo daemon start');
270
+ },
198
271
  'Embedding Coverage Truth': async () => {
199
272
  // Same as the existing Embeddings fix — rebuild the cache by re-running
200
273
  // the embeddings pipeline. Routes through `npx moflo` so the consumer
@@ -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 sql.js storage with automatic embedding generation
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 sql.js directly for consistent data access
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 sql.js search with vector similarity
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 sql.js directly for consistent data access
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 sql.js directly for consistent data access (Issue #980)
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 sql.js
1037
+ // Init subcommand - initialize memory database using node:sqlite
1038
1038
  const initMemoryCommand = {
1039
1039
  name: 'init',
1040
- description: 'Initialize memory database with sql.js (WASM SQLite) - includes vector embeddings, pattern learning, temporal decay',
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 (sql.js)`,
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(process.cwd(), '.moflo', 'neural');
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(process.cwd(), '.moflo', 'memory');
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(process.cwd(), '.moflo', 'memory');
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
  }