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
@@ -13,7 +13,7 @@
13
13
  | `init` | 4 | Project initialization with wizard, presets, skills, hooks |
14
14
  | `agent` | 8 | Agent lifecycle (spawn, list, status, stop, metrics, pool, health, logs) |
15
15
  | `swarm` | 6 | Multi-agent swarm coordination and orchestration |
16
- | `memory` | 11 | sql.js + HNSW vector search, 150x-12,500x faster |
16
+ | `memory` | 11 | node:sqlite + HNSW vector search, 150x-12,500x faster |
17
17
  | `mcp` | 9 | MCP server management and tool execution |
18
18
  | `task` | 6 | Task creation, assignment, and lifecycle |
19
19
  | `session` | 7 | Session state management and persistence |
@@ -9,7 +9,7 @@
9
9
  Source files (`.claude/guidance/*.md`, `docs/**/*.md`, code, tests) flow through four stages:
10
10
 
11
11
  1. **Index** — `bin/index-*.mjs` chunks markdown on `##` headers and walks code/tests for structural facts
12
- 2. **Store** — entries land in `.moflo/moflo.db` (SQLite via sql.js) with metadata + RAG links
12
+ 2. **Store** — entries land in `.moflo/moflo.db` (SQLite via Node 22's built-in `node:sqlite`) with metadata + RAG links
13
13
  3. **Embed** — `bin/build-embeddings.mjs` generates 384-dim vectors via `fastembed` (mandatory, see ADR-EMB-001)
14
14
  4. **Search** — `mcp__moflo__memory_search` (preferred), `npx flo memory search` (fallback), or `npx flo-search` (verbose) hit an HNSW index for nearest-neighbor lookup
15
15
 
@@ -47,7 +47,7 @@ auto_index:
47
47
 
48
48
  # Memory backend
49
49
  memory:
50
- backend: sql.js # sql.js (WASM) | json
50
+ backend: node-sqlite # node-sqlite (default) | rvf (pure-TS fallback) | json (last resort). Passed to createDatabase() as the preferred provider (#1144).
51
51
  embedding_model: Xenova/all-MiniLM-L6-v2 # 384-dim neural embeddings
52
52
  namespace: default # Default namespace for memory operations
53
53
 
@@ -101,9 +101,17 @@ status_line:
101
101
  sandbox:
102
102
  enabled: false # Set to true to wrap bash steps in an OS sandbox
103
103
  tier: auto # auto | denylist-only | full
104
+
105
+ # Auto-update on session start (refreshes consumer assets when moflo upgrades)
106
+ auto_update:
107
+ enabled: true # Master toggle for version-change auto-sync
108
+ scripts: true # Sync .claude/scripts/ from moflo bin/
109
+ helpers: true # Sync .claude/helpers/ from moflo source
110
+ hook_block_drift: warn # warn | regenerate | off
111
+ claudemd_injection_drift: regenerate # warn | regenerate | off
104
112
  ```
105
113
 
106
- If your `moflo.yaml` predates the `sandbox:` block, it is auto-appended on the next session start — you never need to re-run `moflo init` after a version bump.
114
+ If your `moflo.yaml` predates the `sandbox:` or `auto_update:` blocks, they are auto-appended on the next session start — you never need to re-run `moflo init` after a version bump.
107
115
 
108
116
  ### Key Behaviors
109
117
 
@@ -128,6 +136,12 @@ If your `moflo.yaml` predates the `sandbox:` block, it is auto-appended on the n
128
136
  | `sandbox.enabled: true` | Wrap bash steps in an OS sandbox (macOS/Linux/WSL) — absolute disable when `false`, regardless of tier |
129
137
  | `sandbox.tier: full` | Require OS sandbox; throw at runtime if the platform tool is unavailable |
130
138
  | `sandbox.tier: denylist-only` | Keep Layer 1 denylist only; skip OS isolation even when enabled |
139
+ | `auto_update.enabled: false` | Disable all on-session auto-sync (scripts, helpers, drift checks) |
140
+ | `auto_update.hook_block_drift: regenerate` | Auto-repair drift in `.claude/settings.json` hook block on session start (#881) |
141
+ | `auto_update.hook_block_drift: off` | Skip hook-block drift detection entirely |
142
+ | `auto_update.claudemd_injection_drift: regenerate` | Auto-refresh the MoFlo block in `CLAUDE.md` when it drifts from the current generator (#1142, default) |
143
+ | `auto_update.claudemd_injection_drift: warn` | Print a drift notice on session start but leave `CLAUDE.md` unchanged |
144
+ | `auto_update.claudemd_injection_drift: off` | Skip CLAUDE.md injection drift detection entirely |
131
145
 
132
146
  ---
133
147
 
@@ -142,9 +156,9 @@ CLAUDE_FLOW_LOG_LEVEL=info # debug | info | warn | error
142
156
  # MCP Server (stdio transport — no port)
143
157
  CLAUDE_FLOW_MCP_TRANSPORT=stdio
144
158
 
145
- # Memory backend
146
- CLAUDE_FLOW_MEMORY_BACKEND=hybrid # hybrid | sqlite | agentdb (legacy)
147
- CLAUDE_FLOW_MEMORY_TYPE=sqlite # storage type override
159
+ # Memory backend (legacy SystemConfig env vars — moflo.yaml `memory.backend` is the modern surface)
160
+ CLAUDE_FLOW_MEMORY_BACKEND=sqlite # informational only; not consumed by selectProvider
161
+ CLAUDE_FLOW_MEMORY_TYPE=sqlite # SystemConfig override (legacy)
148
162
  ```
149
163
 
150
164
  Variable names retain the `CLAUDE_FLOW_` prefix for backward compatibility with consumers upgraded from claude-flow.
@@ -112,7 +112,7 @@ const stats = await mcp.memory_stats({});
112
112
  - **Don't rebuild the index on every test.** Use a module-level singleton + `beforeAll`. HNSW cold-boot is ~5s.
113
113
  - **Don't raise `ef` globally.** Raise it on the specific queries that need recall. Default is fine for 90% of calls.
114
114
  - **Don't quantize a small corpus.** Below ~500k vectors the RAM saving doesn't justify the recall cost.
115
- - **Don't measure in dev mode.** sql.js WASM behaves differently under `NODE_ENV=production`; benches should match the target.
115
+ - **Don't measure in dev mode.** The memory stack behaves differently under `NODE_ENV=production`; benches should match the target.
116
116
 
117
117
  ## See Also
118
118
 
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: "memory-patterns"
3
- description: "Persistent memory patterns for moflo agents — session memory, long-term knowledge, pattern learning, and cross-session context via moflo's sql.js + HNSW vector store. Use when building stateful agents or assistants that need to remember across runs."
3
+ description: "Persistent memory patterns for moflo agents — session memory, long-term knowledge, pattern learning, and cross-session context via moflo's node:sqlite + HNSW vector store. Use when building stateful agents or assistants that need to remember across runs."
4
4
  ---
5
5
 
6
6
  # MoFlo Memory Patterns
7
7
 
8
- Persistent, semantically-searchable memory for moflo-enabled projects. Backed by `.swarm/memory.db` (sql.js + HNSW vector index) and exposed through MCP tools.
8
+ Persistent, semantically-searchable memory for moflo-enabled projects. Backed by `.moflo/moflo.db` (node:sqlite + HNSW vector index) and exposed through MCP tools.
9
9
 
10
10
  ## Core API
11
11
 
@@ -124,7 +124,7 @@ This is the same fan-out the `/flo` spell does — cheap (HNSW, parallel) and re
124
124
 
125
125
  ## Persistence & Indexing
126
126
 
127
- - File: `.swarm/memory.db` at project root (sql.js).
127
+ - File: `.moflo/moflo.db` at project root (node:sqlite, Node 22+ built-in).
128
128
  - Embeddings: built by cli's embeddings module; indexed with HNSW from `src/cli/memory/`.
129
129
  - Cold-start cost: ~5 seconds to initialize HNSW. Tests should share a single instance (`beforeAll`, not `beforeEach`).
130
130
  - Namespace isolation: each namespace is a logical partition, but the HNSW index spans the table. Query time scales with `limit` and `threshold`, not total row count.
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: "vector-search"
3
- description: "Semantic vector search with moflo — RAG over your own documents, similarity matching, context-aware retrieval via HNSW (sql.js-backed). Use when building retrieval layers for chat, search, or context-assembly."
3
+ description: "Semantic vector search with moflo — RAG over your own documents, similarity matching, context-aware retrieval via HNSW (node:sqlite-backed). Use when building retrieval layers for chat, search, or context-assembly."
4
4
  ---
5
5
 
6
6
  # MoFlo Vector Search (RAG)
7
7
 
8
- Semantic search over your own documents, backed by moflo's HNSW index in `.swarm/memory.db`. Small enough to ship in a devDependency; fast enough for interactive retrieval at 100k–1M vectors.
8
+ Semantic search over your own documents, backed by moflo's HNSW index in `.moflo/moflo.db` (node:sqlite, Node 22+ built-in). Small enough to ship in a devDependency; fast enough for interactive retrieval at 100k–1M vectors.
9
9
 
10
10
  ## When to Use This vs `memory-patterns`
11
11
 
package/README.md CHANGED
@@ -30,7 +30,7 @@ MoFlo makes deliberate choices so you don't have to:
30
30
  - **Fully self-contained** — No external services, no cloud dependencies, no API keys. Everything runs locally on your machine.
31
31
  - **Minimal dependencies** — small runtime dep set, all WASM or prebuilt binaries. No native compilation, no `node-gyp`, no platform-specific build steps.
32
32
  - **Node.js runtime** — Targets Node.js specifically. All scripts, hooks, and tooling are JavaScript/TypeScript. No Python, no Rust binaries, no native compilation.
33
- - **sql.js (WASM)** — The memory database uses sql.js, a pure WebAssembly build of SQLite. No native `better-sqlite3` bindings to compile, no platform-specific build steps. Works identically on Windows, macOS, and Linux.
33
+ - **node:sqlite (built-in)** — The memory database uses Node 22's built-in SQLite engine. No `better-sqlite3` native bindings to compile, no WASM round-trip, no platform-specific build steps. Works identically on Windows, macOS, and Linux.
34
34
  - **Neural embeddings by default** — 384-dimensional embeddings using `all-MiniLM-L6-v2`. No hash fallback, no peer-optional setup, no install prompts — real semantic search works out of the box. A `postinstall` step trims the embedding runtime to your platform and strips GPU-only libraries the runtime never loads, reclaiming roughly 340 MB on Linux and 150 MB on Windows from a fresh install. Set `MOFLO_NO_PRUNE=1` to skip the trim, or `ONNXRUNTIME_NODE_INSTALL_CUDA=true` to keep CUDA GPU support.
35
35
  - **Full learning stack wired up OOTB** — All configured and functional from `flo init`, no manual setup:
36
36
  - **SONA** (Self-Optimizing Neural Architecture) — learns from task trajectories
@@ -537,7 +537,7 @@ flo diagnose --json # JSON output for CI/automation
537
537
  | **MCP Spell Integration** | Bridge between MCP tools and spell engine functions correctly |
538
538
  | **Hook Execution** | Hook executor is functional and can fire hooks |
539
539
  | **Gate Health** | All gate cases, hook bindings, and state file are intact |
540
- | **MofloDb Bridge** | Memory DB adapter (sql.js + HNSW) is wired and routable |
540
+ | **MofloDb Bridge** | Memory DB adapter (node:sqlite + HNSW) is wired and routable |
541
541
  | **Sandbox Tier** | Detects which sandbox backend is available (Docker / bwrap / sandbox-exec / none) |
542
542
 
543
543
  **Auto-fix mode** (`flo healer --fix`) attempts to repair each failing check automatically:
@@ -645,7 +645,7 @@ These are the backend systems that hooks and commands interact with.
645
645
 
646
646
  | System | What It Does | Why It Matters | Enabled OOTB |
647
647
  |--------|-------------|----------------|:---:|
648
- | **Semantic Memory** | SQLite database (sql.js/WASM) storing knowledge entries with 384-dim vector embeddings | Your AI assistant accumulates project knowledge across sessions instead of starting from scratch each time | Yes |
648
+ | **Semantic Memory** | SQLite database (Node's built-in `node:sqlite`) storing knowledge entries with 384-dim vector embeddings | Your AI assistant accumulates project knowledge across sessions instead of starting from scratch each time | Yes |
649
649
  | **HNSW Vector Search** | Hierarchical Navigable Small World index for fast nearest-neighbor search | Searches across thousands of stored entries return in milliseconds instead of scanning linearly | Yes |
650
650
  | **Guidance Indexing** | Chunks markdown docs into overlapping segments, embeds each with MiniLM-L6-v2 | Your project documentation becomes searchable by meaning ("how does auth work?") not just keywords | Yes |
651
651
  | **Code Map** | Parses source files for exports, classes, functions, types | The AI can answer "where is X defined?" from the index instead of running Glob/Grep | Yes |
@@ -720,7 +720,7 @@ Routing outcomes persist across sessions. You can inspect them with `flo hooks p
720
720
 
721
721
  ### Memory & Knowledge Storage
722
722
 
723
- MoFlo uses a SQLite database (via sql.js/WASM — no native deps) to store three types of knowledge:
723
+ MoFlo uses a SQLite database (via Node 22's built-in `node:sqlite` — no native deps) to store three types of knowledge:
724
724
 
725
725
  | Namespace | What's Stored | How It Gets There |
726
726
  |-----------|---------------|-------------------|
@@ -861,7 +861,7 @@ MoFlo started from [Ruflo/Claude Flow](https://github.com/ruvnet/ruflo) but is n
861
861
 
862
862
  My use case was just one of those many scenarios: day-to-day local coding, enhancing my normal Claude Code experience on a single project. The original supported this — it was all in there — but because the project served so many different needs, I found myself configuring and tailoring things for my specific setup each time I pulled in updates. That isn't a shortcoming of the original; it's the natural trade-off of a tool designed to be that flexible and powerful.
863
863
 
864
- So I started from that foundation and narrowed the focus to my particular corner of it. I baked in the defaults I kept setting manually, added automatic indexing and memory gating at session start, and tuned the out-of-box experience so that `npm install` and `flo init` gets you straight to coding. Over time MoFlo grew its own architecture (workspace collapse, in-tree fastembed runtime, sql.js + HNSW memory layer, spell engine, daemon-driven scheduling) and the two projects fully diverged.
864
+ So I started from that foundation and narrowed the focus to my particular corner of it. I baked in the defaults I kept setting manually, added automatic indexing and memory gating at session start, and tuned the out-of-box experience so that `npm install` and `flo init` gets you straight to coding. Over time MoFlo grew its own architecture (workspace collapse, in-tree fastembed runtime, node:sqlite + HNSW memory layer, spell engine, daemon-driven scheduling) and the two projects fully diverged.
865
865
 
866
866
  If you're exploring the full breadth of agent orchestration, go look at [Ruflo/Claude Flow](https://github.com/ruvnet/ruflo) — it's the real deal. If your needs are similar to mine — a focused, opinionated local dev setup that just works — MoFlo is for you.
867
867
 
@@ -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
+ }
@@ -640,7 +640,16 @@ try {
640
640
  // Controlled by `auto_update.enabled` in moflo.yaml (default: true).
641
641
  // When moflo is upgraded (npm install), scripts and helpers may be stale.
642
642
  // Detect version change and sync from source before running hooks.
643
- let autoUpdateConfig = { enabled: true, scripts: true, helpers: true, hookBlockDrift: 'warn' };
643
+ let autoUpdateConfig = {
644
+ enabled: true,
645
+ scripts: true,
646
+ helpers: true,
647
+ hookBlockDrift: 'warn',
648
+ // #1142 — CLAUDE.md injection drift refresh mode (warn | regenerate | off,
649
+ // default regenerate). Defaults to regenerate because the consumer cannot
650
+ // refresh CLAUDE.md on their own — there is no other auto-refresh path.
651
+ claudemdInjectionDrift: 'regenerate',
652
+ };
644
653
  try {
645
654
  const mofloYaml = resolve(projectRoot, 'moflo.yaml');
646
655
  if (existsSync(mofloYaml)) {
@@ -651,10 +660,13 @@ try {
651
660
  const helpersMatch = yamlContent.match(/auto_update:\s*\n(?:\s+\w+:.*\n)*?\s+helpers:\s*(true|false)/);
652
661
  // #881: hook-block drift detector (warn | regenerate | off; default warn)
653
662
  const driftMatch = yamlContent.match(/auto_update:\s*\n(?:\s+\w+:.*\n)*?\s+hook_block_drift:\s*(warn|regenerate|off)/);
663
+ // #1142: CLAUDE.md injection drift detector (warn | regenerate | off; default regenerate)
664
+ const claudemdMatch = yamlContent.match(/auto_update:\s*\n(?:\s+\w+:.*\n)*?\s+claudemd_injection_drift:\s*(warn|regenerate|off)/);
654
665
  if (enabledMatch) autoUpdateConfig.enabled = enabledMatch[1] === 'true';
655
666
  if (scriptsMatch) autoUpdateConfig.scripts = scriptsMatch[1] === 'true';
656
667
  if (helpersMatch) autoUpdateConfig.helpers = helpersMatch[1] === 'true';
657
668
  if (driftMatch) autoUpdateConfig.hookBlockDrift = driftMatch[1];
669
+ if (claudemdMatch) autoUpdateConfig.claudemdInjectionDrift = claudemdMatch[1];
658
670
  }
659
671
  } catch (err) {
660
672
  // Defaults (all true) keep the upgrade flow alive but the user should
@@ -1414,23 +1426,185 @@ async function runHookBlockDriftCheck() {
1414
1426
  };
1415
1427
  }
1416
1428
 
1417
- try {
1418
- if (autoUpdateConfig.enabled && autoUpdateConfig.hookBlockDrift !== 'off') {
1419
- const result = await runHookBlockDriftCheck();
1420
- if (result) {
1429
+ // ── 3a-vii. CLAUDE.md injection drift detection (#1142) ──────────────────
1430
+ // Refresh the consumer's `<root>/CLAUDE.md` MoFlo block when it has drifted
1431
+ // from what `claudemd-generator.ts` currently produces. The launcher's
1432
+ // stages 3/3b refresh shipped guidance files on every version change, but
1433
+ // CLAUDE.md was only rewritten by explicit `flo init` / `flo-setup` — so
1434
+ // consumers carried stale injection content (with the legacy `shipped/`
1435
+ // guidance paths, for example) for as long as they didn't re-run init.
1436
+ //
1437
+ // Modes (`auto_update.claudemd_injection_drift` in moflo.yaml):
1438
+ // regenerate — replace the drifted block in place (default; the consumer
1439
+ // has no other path to refresh CLAUDE.md)
1440
+ // warn — print a one-line drift summary to stdout
1441
+ // off — skip detection entirely
1442
+ //
1443
+ // Fast-path: `.moflo/claudemd-injection-cache.json` records the last clean
1444
+ // run. If CLAUDE.md + the generator module both still match the cached
1445
+ // mtimes, skip the readFile + dynamic import.
1446
+ async function runClaudeMdInjectionDriftCheck() {
1447
+ const claudeMdPath = resolve(projectRoot, 'CLAUDE.md');
1448
+ let claudeMdStat;
1449
+ try { claudeMdStat = statSync(claudeMdPath); } catch { return null; }
1450
+
1451
+ // Locate the generator and the drift service. Both must be present —
1452
+ // generator owns the canonical content, drift service owns the marker
1453
+ // logic. Use bin/lib/moflo-resolve.mjs path resolution semantics
1454
+ // (covers consumer node_modules + dev source tree). Two candidates each.
1455
+ const generatorCandidates = [
1456
+ resolve(projectRoot, 'node_modules/moflo/dist/src/cli/init/claudemd-generator.js'),
1457
+ resolve(projectRoot, 'dist/src/cli/init/claudemd-generator.js'),
1458
+ ];
1459
+ const driftCandidates = [
1460
+ resolve(projectRoot, 'node_modules/moflo/dist/src/cli/services/claudemd-injection.js'),
1461
+ resolve(projectRoot, 'dist/src/cli/services/claudemd-injection.js'),
1462
+ ];
1463
+ let generatorPath = null, generatorStat = null;
1464
+ for (const p of generatorCandidates) {
1465
+ try { generatorStat = statSync(p); generatorPath = p; break; } catch { /* try next */ }
1466
+ }
1467
+ let driftPath = null, driftStat = null;
1468
+ for (const p of driftCandidates) {
1469
+ try { driftStat = statSync(p); driftPath = p; break; } catch { /* try next */ }
1470
+ }
1471
+ if (!generatorPath || !driftPath) return null;
1472
+
1473
+ // Use the max mtime of the two modules so any update to either invalidates
1474
+ // the cache. Both are co-bumped on publish so this is normally one mtime.
1475
+ const moduleMtimeMs = Math.max(generatorStat.mtimeMs, driftStat.mtimeMs);
1476
+
1477
+ // Cache short-circuits any (claudeMdMtime, moduleMtime, state) triple
1478
+ // match — not just 'in-sync'. A drifted consumer in warn mode still emits
1479
+ // the nudge once on first detection, then stays silent until something
1480
+ // actually changes (CLAUDE.md mtime bumps from a user edit, or moflo
1481
+ // upgrade bumps moduleMtimeMs). Without this, every session re-does the
1482
+ // full slow path (3 statSync + readFile + 2 dynamic imports + generator
1483
+ // call) for non-in-sync consumers in perpetuity.
1484
+ const cachePath = join(mofloDir(projectRoot), 'claudemd-injection-cache.json');
1485
+ let cached = null;
1486
+ try { cached = JSON.parse(readFileSync(cachePath, 'utf-8')); } catch { /* missing or corrupt */ }
1487
+ if (
1488
+ cached &&
1489
+ cached.claudeMdMtimeMs === claudeMdStat.mtimeMs &&
1490
+ cached.moduleMtimeMs === moduleMtimeMs &&
1491
+ typeof cached.state === 'string'
1492
+ ) return null;
1493
+
1494
+ // Try-catch around the dynamic imports handles the file disappearing
1495
+ // between statSync and import (TOCTOU); other load errors surface as
1496
+ // an emitWarning so a transitive dependency failure isn't invisible
1497
+ // (mirrors the silent-catch lesson — see feedback_consumer_blast_radius).
1498
+ let genMod = null, driftMod = null;
1499
+ try {
1500
+ genMod = await import(pathToFileURL(generatorPath).href);
1501
+ driftMod = await import(pathToFileURL(driftPath).href);
1502
+ } catch (err) {
1503
+ emitWarning(`CLAUDE.md drift check skipped (${errMessage(err)})`);
1504
+ return null;
1505
+ }
1506
+ if (typeof genMod.generateClaudeMd !== 'function') return null;
1507
+ if (typeof driftMod.computeInjectionDrift !== 'function') return null;
1508
+ if (typeof driftMod.applyInjectionReplacement !== 'function') return null;
1509
+
1510
+ const claudeMdContents = readFileSync(claudeMdPath, 'utf-8');
1511
+ const canonical = genMod.generateClaudeMd({});
1512
+ const report = driftMod.computeInjectionDrift(claudeMdContents, canonical);
1513
+
1514
+ let finalState = report.state;
1515
+ let finalMtime = claudeMdStat.mtimeMs;
1516
+
1517
+ // Treat both 'drifted' and 'legacy-marker' as repairable in regenerate mode.
1518
+ // 'no-marker' means the user removed the inject deliberately — don't re-add
1519
+ // it on every session start; that's a re-init operation, not a drift fix.
1520
+ // 'no-file' is unreachable here because statSync already succeeded.
1521
+ const repairable = report.state === 'drifted' || report.state === 'legacy-marker';
1522
+ if (repairable) {
1523
+ const wantRegenerate = autoUpdateConfig.claudemdInjectionDrift === 'regenerate';
1524
+ if (wantRegenerate) {
1525
+ const result = driftMod.applyInjectionReplacement(claudeMdContents, canonical);
1526
+ if (result.changed && typeof result.contents === 'string') {
1527
+ writeFileSync(claudeMdPath, result.contents);
1528
+ finalState = 'in-sync';
1529
+ try { finalMtime = statSync(claudeMdPath).mtimeMs; } catch { /* keep prior */ }
1530
+ emitMutation(
1531
+ 'refreshed CLAUDE.md MoFlo block',
1532
+ `replaced ${report.state} block with current generator output`,
1533
+ );
1534
+ }
1535
+ } else {
1536
+ // warn mode — surface a one-line summary on stdout for Claude/user.
1421
1537
  try {
1422
- mkdirSync(mofloDir(projectRoot), { recursive: true });
1423
- writeFileSync(result.cachePath, JSON.stringify({
1424
- settingsMtimeMs: result.settingsMtimeMs,
1425
- moduleMtimeMs: result.moduleMtimeMs,
1426
- consumerHash: result.consumerHash,
1427
- referenceHash: result.referenceHash,
1428
- }));
1429
- } catch { /* cache is opportunistic non-fatal */ }
1538
+ process.stdout.write(
1539
+ `moflo: CLAUDE.md injection ${report.state}; run \`flo doctor claudemd-drift\` or set auto_update.claudemd_injection_drift: regenerate in moflo.yaml\n`,
1540
+ );
1541
+ } catch { /* broken stdout — non-fatal */ }
1542
+ }
1543
+ } else if (report.state === 'no-marker') {
1544
+ // Distinct from the drift cases — surface once via warn channel so a
1545
+ // user who didn't run init still sees a nudge, but never auto-mutate.
1546
+ if (autoUpdateConfig.claudemdInjectionDrift !== 'off') {
1547
+ try {
1548
+ process.stdout.write(
1549
+ `moflo: CLAUDE.md has no MoFlo injection block; run \`npx flo-setup\` to add one\n`,
1550
+ );
1551
+ } catch { /* broken stdout — non-fatal */ }
1430
1552
  }
1431
1553
  }
1432
- } catch (err) {
1433
- emitWarning(`hook-block drift check skipped (${errMessage(err)})`);
1554
+
1555
+ return {
1556
+ cachePath,
1557
+ claudeMdMtimeMs: finalMtime,
1558
+ moduleMtimeMs,
1559
+ state: finalState,
1560
+ };
1561
+ }
1562
+
1563
+ // Run the two drift detectors (settings.json hook block + CLAUDE.md
1564
+ // injection) in parallel. Both do their own statSync → cache compare →
1565
+ // dynamic import dance; the work is independent and the file targets are
1566
+ // different, so Promise.all halves cold-path latency on every session start.
1567
+ // Cache-hit fast paths return null with no work — Promise.all is still
1568
+ // trivially correct there.
1569
+ {
1570
+ const hookEnabled = autoUpdateConfig.enabled && autoUpdateConfig.hookBlockDrift !== 'off';
1571
+ const claudemdEnabled = autoUpdateConfig.enabled && autoUpdateConfig.claudemdInjectionDrift !== 'off';
1572
+ const [hookResult, claudemdResult] = await Promise.all([
1573
+ hookEnabled
1574
+ ? runHookBlockDriftCheck().catch((err) => {
1575
+ emitWarning(`hook-block drift check skipped (${errMessage(err)})`);
1576
+ return null;
1577
+ })
1578
+ : Promise.resolve(null),
1579
+ claudemdEnabled
1580
+ ? runClaudeMdInjectionDriftCheck().catch((err) => {
1581
+ emitWarning(`CLAUDE.md injection drift check skipped (${errMessage(err)})`);
1582
+ return null;
1583
+ })
1584
+ : Promise.resolve(null),
1585
+ ]);
1586
+
1587
+ if (hookResult) {
1588
+ try {
1589
+ mkdirSync(mofloDir(projectRoot), { recursive: true });
1590
+ writeFileSync(hookResult.cachePath, JSON.stringify({
1591
+ settingsMtimeMs: hookResult.settingsMtimeMs,
1592
+ moduleMtimeMs: hookResult.moduleMtimeMs,
1593
+ consumerHash: hookResult.consumerHash,
1594
+ referenceHash: hookResult.referenceHash,
1595
+ }));
1596
+ } catch { /* cache is opportunistic — non-fatal */ }
1597
+ }
1598
+ if (claudemdResult) {
1599
+ try {
1600
+ mkdirSync(mofloDir(projectRoot), { recursive: true });
1601
+ writeFileSync(claudemdResult.cachePath, JSON.stringify({
1602
+ claudeMdMtimeMs: claudemdResult.claudeMdMtimeMs,
1603
+ moduleMtimeMs: claudemdResult.moduleMtimeMs,
1604
+ state: claudemdResult.state,
1605
+ }));
1606
+ } catch { /* cache is opportunistic — non-fatal */ }
1607
+ }
1434
1608
  }
1435
1609
 
1436
1610
  // ── 3b. Ensure shipped guidance files exist (even without version change) ──
@@ -37,16 +37,14 @@ import { mofloInternalURL } from './lib/moflo-resolve.mjs';
37
37
  // works identically from bin/ (canonical) or from .claude/scripts/ (synced copy).
38
38
  const mofloRoot = dirname(fileURLToPath(mofloInternalURL('package.json')));
39
39
 
40
- // Single source of truth: claudemd-generator.ts owns the section content.
41
- // Use the shared mofloInternalURL helper so the script works identically when
42
- // invoked from bin/ (canonical) or from .claude/scripts/ (synced copy).
43
- const {
44
- generateClaudeMd,
45
- MARKER_START,
46
- MARKER_END,
47
- LEGACY_MARKER_STARTS,
48
- LEGACY_MARKER_ENDS,
49
- } = await import(mofloInternalURL('dist/src/cli/init/claudemd-generator.js'));
40
+ // Single source of truth: claudemd-generator.ts owns the section content,
41
+ // claudemd-injection.ts owns the marker-replace logic. Use the shared
42
+ // mofloInternalURL helper so the script works identically when invoked
43
+ // from bin/ (canonical) or from .claude/scripts/ (synced copy).
44
+ const { generateClaudeMd } = await import(mofloInternalURL('dist/src/cli/init/claudemd-generator.js'));
45
+ const { applyInjectionReplacement, computeInjectionDrift } = await import(
46
+ mofloInternalURL('dist/src/cli/services/claudemd-injection.js')
47
+ );
50
48
 
51
49
  const args = process.argv.slice(2);
52
50
  const updateOnly = args.includes('--update');
@@ -150,65 +148,47 @@ function cleanupLegacyBootstrap(projectRoot) {
150
148
 
151
149
  function updateClaudeMd(projectRoot) {
152
150
  const claudeMdPath = join(projectRoot, 'CLAUDE.md');
151
+ const existed = existsSync(claudeMdPath);
152
+ const content = existed ? readFileSync(claudeMdPath, 'utf-8') : null;
153
153
 
154
- if (!existsSync(claudeMdPath)) {
155
- if (checkOnly) {
156
- log('⚠️ No CLAUDE.md found');
157
- return false;
158
- }
159
- log('📝 Creating CLAUDE.md with subagent protocol section');
160
- writeFileSync(claudeMdPath, `# Project Configuration\n\n${CLAUDE_MD_SECTION}\n`, 'utf-8');
161
- return true;
162
- }
154
+ // Single source of truth for the marker-replace logic lives in
155
+ // src/cli/services/claudemd-injection.ts. Classify state for logging,
156
+ // then apply (or report) the replacement.
157
+ const report = computeInjectionDrift(content, CLAUDE_MD_SECTION);
163
158
 
164
- const content = readFileSync(claudeMdPath, 'utf-8');
165
-
166
- // Check for current or legacy markers and replace
167
- const allStarts = [MARKER_START, ...LEGACY_MARKER_STARTS];
168
- const allEnds = [MARKER_END, ...LEGACY_MARKER_ENDS];
169
-
170
- for (let i = 0; i < allStarts.length; i++) {
171
- if (content.includes(allStarts[i])) {
172
- const startIdx = content.indexOf(allStarts[i]);
173
- const endIdx = content.indexOf(allEnds[i]);
174
-
175
- if (endIdx > startIdx) {
176
- // If current markers and content matches, we're up to date
177
- if (i === 0) {
178
- const existingSection = content.substring(startIdx, endIdx + allEnds[i].length);
179
- if (existingSection === CLAUDE_MD_SECTION) {
180
- log('✅ CLAUDE.md moflo section is current');
181
- return true;
182
- }
183
- }
184
-
185
- // Replace (current or legacy) with new section
186
- if (!checkOnly) {
187
- const updated = content.substring(0, startIdx) + CLAUDE_MD_SECTION + content.substring(endIdx + allEnds[i].length);
188
- writeFileSync(claudeMdPath, updated, 'utf-8');
189
- log(i === 0 ? '📝 Updated CLAUDE.md moflo section' : '📝 Replaced legacy CLAUDE.md section with minimal moflo injection');
190
- } else {
191
- log('⚠️ CLAUDE.md moflo section needs update');
192
- }
193
- return true;
194
- }
195
- }
159
+ if (report.state === 'in-sync') {
160
+ log('✅ CLAUDE.md moflo section is current');
161
+ return true;
196
162
  }
197
163
 
164
+ // `updateOnly` is informational — refresh the bootstrap mirror file but
165
+ // leave CLAUDE.md alone unless an inject already exists.
198
166
  if (updateOnly) {
199
- log('⚠️ CLAUDE.md has no moflo section (run without --update to add)');
200
- return false;
167
+ if (!existed) { log('⚠️ No CLAUDE.md found'); return false; }
168
+ if (report.state === 'no-marker') {
169
+ log('⚠️ CLAUDE.md has no moflo section (run without --update to add)');
170
+ return false;
171
+ }
172
+ // Existing block (current or legacy) + drift → fall through to write.
201
173
  }
202
174
 
203
175
  if (checkOnly) {
204
- log('⚠️ CLAUDE.md missing subagent protocol section');
176
+ if (!existed) { log('⚠️ No CLAUDE.md found'); return false; }
177
+ if (report.state === 'no-marker') { log('⚠️ CLAUDE.md missing subagent protocol section'); return false; }
178
+ log('⚠️ CLAUDE.md moflo section needs update');
205
179
  return false;
206
180
  }
207
181
 
208
- // Append section to end of CLAUDE.md
209
- const separator = content.endsWith('\n') ? '\n' : '\n\n';
210
- writeFileSync(claudeMdPath, content + separator + CLAUDE_MD_SECTION + '\n', 'utf-8');
211
- log('📝 Added subagent protocol section to CLAUDE.md');
182
+ const result = applyInjectionReplacement(content, CLAUDE_MD_SECTION);
183
+ if (!result.changed) return true;
184
+ writeFileSync(claudeMdPath, result.contents, 'utf-8');
185
+
186
+ switch (report.state) {
187
+ case 'no-file': log('📝 Creating CLAUDE.md with subagent protocol section'); break;
188
+ case 'no-marker': log('📝 Added subagent protocol section to CLAUDE.md'); break;
189
+ case 'legacy-marker': log('📝 Replaced legacy CLAUDE.md section with minimal moflo injection'); break;
190
+ case 'drifted': log('📝 Updated CLAUDE.md moflo section'); break;
191
+ }
212
192
  return true;
213
193
  }
214
194