moflo 4.9.36 → 4.10.0

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 (79) hide show
  1. package/.claude/guidance/shipped/moflo-agent-rules.md +12 -0
  2. package/.claude/guidance/shipped/moflo-memory-protocol.md +34 -0
  3. package/.claude/guidance/shipped/moflo-memorydb-maintenance.md +22 -11
  4. package/.claude/guidance/shipped/moflo-root-cause-discipline.md +47 -0
  5. package/.claude/guidance/shipped/moflo-subagents.md +4 -0
  6. package/.claude/helpers/gate.cjs +3 -3
  7. package/.claude/helpers/statusline.cjs +69 -33
  8. package/.claude/helpers/subagent-bootstrap.json +1 -1
  9. package/.claude/helpers/subagent-start.cjs +1 -1
  10. package/.claude/skills/eldar/SKILL.md +8 -0
  11. package/bin/build-embeddings.mjs +6 -20
  12. package/bin/cli.js +5 -0
  13. package/bin/gate.cjs +3 -3
  14. package/bin/generate-code-map.mjs +4 -24
  15. package/bin/hooks.mjs +3 -12
  16. package/bin/index-all.mjs +3 -13
  17. package/bin/index-guidance.mjs +59 -119
  18. package/bin/index-patterns.mjs +6 -24
  19. package/bin/index-tests.mjs +4 -23
  20. package/bin/lib/db-repair.mjs +4 -25
  21. package/bin/lib/get-backend.mjs +306 -0
  22. package/bin/lib/incremental-write.mjs +27 -7
  23. package/bin/lib/moflo-paths.mjs +64 -4
  24. package/bin/lib/suppress-sqlite-warning.mjs +57 -0
  25. package/bin/migrations/knowledge-purge.mjs +7 -8
  26. package/bin/migrations/knowledge-to-learnings.mjs +7 -9
  27. package/bin/migrations/purge-doc-entries.mjs +52 -0
  28. package/bin/migrations/strip-context-preambles.mjs +95 -0
  29. package/bin/run-migrations.mjs +1 -10
  30. package/bin/semantic-search.mjs +11 -19
  31. package/bin/session-start-launcher.mjs +102 -100
  32. package/bin/simplify-classify.cjs +38 -17
  33. package/dist/src/cli/commands/daemon.js +38 -11
  34. package/dist/src/cli/commands/doctor-checks-coverage-truth.js +136 -0
  35. package/dist/src/cli/commands/doctor-checks-memory-access.js +244 -5
  36. package/dist/src/cli/commands/doctor-checks-memory.js +13 -18
  37. package/dist/src/cli/commands/doctor-checks-version-skew.js +94 -0
  38. package/dist/src/cli/commands/doctor-checks-writers-audit.js +170 -0
  39. package/dist/src/cli/commands/doctor-embedding-hygiene.js +3 -15
  40. package/dist/src/cli/commands/doctor-fixes.js +30 -0
  41. package/dist/src/cli/commands/doctor-registry.js +14 -0
  42. package/dist/src/cli/commands/doctor.js +1 -1
  43. package/dist/src/cli/commands/embeddings.js +17 -22
  44. package/dist/src/cli/commands/memory.js +54 -75
  45. package/dist/src/cli/embeddings/persistent-cache.js +44 -83
  46. package/dist/src/cli/init/claudemd-generator.js +4 -0
  47. package/dist/src/cli/init/moflo-init.js +40 -0
  48. package/dist/src/cli/mcp-tools/memory-tools.js +177 -32
  49. package/dist/src/cli/memory/bridge-core.js +256 -30
  50. package/dist/src/cli/memory/bridge-entries.js +76 -8
  51. package/dist/src/cli/memory/controller-registry.js +7 -2
  52. package/dist/src/cli/memory/controllers/batch-operations.js +5 -1
  53. package/dist/src/cli/memory/controllers/hierarchical-memory.js +7 -2
  54. package/dist/src/cli/memory/controllers/mutation-guard.js +22 -2
  55. package/dist/src/cli/memory/daemon-backend.js +400 -0
  56. package/dist/src/cli/memory/daemon-write-client.js +192 -15
  57. package/dist/src/cli/memory/database-provider.js +57 -40
  58. package/dist/src/cli/memory/hnsw-persistence.js +6 -8
  59. package/dist/src/cli/memory/index.js +0 -1
  60. package/dist/src/cli/memory/memory-bridge.js +40 -8
  61. package/dist/src/cli/memory/memory-initializer.js +286 -220
  62. package/dist/src/cli/memory/rvf-migration.js +25 -11
  63. package/dist/src/cli/memory/sqlite-backend.js +573 -0
  64. package/dist/src/cli/memory/suppress-sqlite-warning.js +49 -0
  65. package/dist/src/cli/services/cherry-pick-learnings.js +32 -21
  66. package/dist/src/cli/services/daemon-dashboard.js +13 -1
  67. package/dist/src/cli/services/daemon-lock.js +58 -1
  68. package/dist/src/cli/services/daemon-memory-rpc.js +245 -10
  69. package/dist/src/cli/services/embeddings-migration.js +9 -12
  70. package/dist/src/cli/services/ephemeral-namespace-purge.js +6 -11
  71. package/dist/src/cli/services/learning-service.js +12 -20
  72. package/dist/src/cli/services/project-root.js +69 -9
  73. package/dist/src/cli/services/soft-delete-purge.js +6 -11
  74. package/dist/src/cli/services/sqljs-migration-store.js +4 -1
  75. package/dist/src/cli/services/subagent-bootstrap.js +1 -1
  76. package/dist/src/cli/shared/events/event-store.js +26 -55
  77. package/dist/src/cli/version.js +1 -1
  78. package/package.json +2 -4
  79. package/dist/src/cli/memory/sqljs-backend.js +0 -643
@@ -24,6 +24,18 @@
24
24
 
25
25
  **Search `tests` when looking for test coverage** of a function, module, or behavior — it indexes the test tree separately so you can pinpoint specs without grepping the whole repo.
26
26
 
27
+ ### Traverse Chunks, Don't Bulk-Retrieve
28
+
29
+ Search returns chunked guidance with a compact `navigation` crumb (`parentDoc`, `prevChunk`, `nextChunk`, `chunkTitle`). Use it:
30
+
31
+ | Want | Use |
32
+ |------|-----|
33
+ | Adjacent / sibling / hierarchical context | `mcp__moflo__memory_get_neighbors` |
34
+ | Full content of one chunk | `mcp__moflo__memory_retrieve` (returns full nav for further traversal) |
35
+ | Whole source doc | `Read` `parentPath` from any chunk's nav |
36
+
37
+ Full protocol: `.claude/guidance/moflo-memory-protocol.md`. Don't retrieve every search hit blindly.
38
+
27
39
  ### Tool Selection (MCP-first)
28
40
 
29
41
  | Tool | Purpose |
@@ -0,0 +1,34 @@
1
+ # MoFlo Memory Protocol — Search, Traverse, Retrieve
2
+
3
+ **Purpose:** How to use moflo's chunked memory effectively. Search returns navigable chunks — traverse the chunk graph, do not bulk-retrieve every hit.
4
+
5
+ ---
6
+
7
+ ## Rule (MUST)
8
+
9
+ When a search hit carries a non-null `navigation`, you MUST call `mcp__moflo__memory_get_neighbors` to traverse — not `mcp__moflo__memory_retrieve`. `memory_retrieve` is for fetching one specific chunk in full, or for non-chunk entries where `navigation` is null. Bulk-retrieving search hits defeats the chunking architecture.
10
+
11
+ ## Decision Table
12
+
13
+ | You want | Use | Why |
14
+ |----------|-----|-----|
15
+ | Find an entry-point | `mcp__moflo__memory_search` | Returns chunk hits with compact `navigation` (parentDoc, prev/next, chunkTitle) |
16
+ | Adjacent context (1 chunk over) | `mcp__moflo__memory_get_neighbors` `{ key, include: ['prev','next'] }` | One round-trip, returns shaped entries with full nav |
17
+ | Same-section peers (h2/h3 family) | `memory_get_neighbors` `{ include: ['siblings'] }` or `['parent','children']` | Hierarchical traversal — cheaper than re-searching |
18
+ | Full content of one chunk | `mcp__moflo__memory_retrieve` `{ key }` | Returns full nav object for further traversal |
19
+ | Whole source doc when truly needed | `Read` `parentPath` from any chunk's nav | Disk read is cheaper than re-indexed `doc-*` |
20
+
21
+ ## Anti-Patterns
22
+
23
+ | Don't | Do instead |
24
+ |-------|-----------|
25
+ | Retrieve every search hit blindly | Traverse via `memory_get_neighbors` when `navigation` is present — bulk `memory_retrieve` per hit is a protocol violation |
26
+ | Open the source file when a chunk would do | Stay in the chunk graph; `Read` `parentPath` only for the rare full-doc case |
27
+ | Search again for a key you already have | `memory_retrieve` or `memory_get_neighbors` directly |
28
+
29
+ ---
30
+
31
+ ## See Also
32
+
33
+ - `.claude/guidance/moflo-agent-rules.md` § Memory-First Protocol — namespaces, query examples, MCP-first tool selection
34
+ - `.claude/guidance/moflo-memory-strategy.md` — How chunking, embeddings, and the RAG index work
@@ -7,10 +7,26 @@
7
7
  ## Database Location
8
8
 
9
9
  - **Path:** `.moflo/moflo.db` — canonical location
10
- - **Engine:** sql.js (WASM-based SQLite no native binaries needed)
10
+ - **Engine:** `node:sqlite` (built into Node 22+) running in WAL mode — multi-process safe via POSIX/Windows file locks. The sql.js (WASM) backend is no longer used and the npm dep is being removed.
11
11
  - **Single table:** `memory_entries` — stores content, embeddings, and metadata in one table
12
+ - **WAL sidecars:** `.moflo/moflo.db-wal` + `.moflo/moflo.db-shm` appear next to `moflo.db` after the first write. Don't commit them; don't manually delete them while moflo is running.
12
13
  - **Legacy path:** `.swarm/memory.db` — older consumers may still have this. The launcher migrates it to `.moflo/moflo.db` automatically on session start. After migration the legacy file is renamed `.swarm/memory.db.bak` and is safe to delete once you've verified the canonical DB is healthy (`flo doctor`).
13
14
 
15
+ ## Network filesystem caveat
16
+
17
+ SQLite's multi-process safety relies on POSIX/Windows file locking and on shared-memory WAL sidecars. **Neither works reliably on network-mounted filesystems** — NFS, SMB/CIFS, sshfs, and some FUSE mounts silently break advisory locks and refuse to map the `.db-shm` shared-memory region. If `.moflo/moflo.db` lives on a network mount, two moflo processes can clobber each other's writes and you may see partial-row corruption.
18
+
19
+ On first open against a non-WAL-capable filesystem moflo emits a one-line stderr warning naming the path:
20
+
21
+ ```
22
+ [moflo] WARNING: SQLite journal_mode=delete on /mnt/network/proj/.moflo/moflo.db (WAL not active).
23
+ If this directory is on NFS/SMB or another network filesystem, POSIX advisory locks are unreliable
24
+ and concurrent moflo processes can corrupt the database. Move the project to a local disk to
25
+ restore multi-process safety.
26
+ ```
27
+
28
+ The warning is deduplicated per path per process. If you see it: move the project (or just the `.moflo/` directory) onto a local disk. There is no in-product workaround — single-machine SQLite cannot be made safe over a remote filesystem.
29
+
14
30
  ## Schema
15
31
 
16
32
  ```sql
@@ -73,33 +89,28 @@ node bin/build-embeddings.mjs
73
89
 
74
90
  ## Purging a Namespace
75
91
 
76
- Use sql.js directly (no sqlite3 binary required):
92
+ Use the backend factory so you get the same engine + WAL semantics every other writer uses:
77
93
 
78
94
  ```js
79
95
  node --input-type=module -e "
80
- import initSqlJs from 'sql.js';
81
- import { readFileSync, writeFileSync } from 'fs';
96
+ import { openBackend } from 'moflo/bin/lib/get-backend.mjs';
82
97
 
83
- const SQL = await initSqlJs();
84
- const buf = readFileSync('.moflo/moflo.db');
85
- const db = new SQL.Database(buf);
98
+ const db = await openBackend(process.cwd(), { create: false });
86
99
 
87
- // Check counts before
88
100
  const before = db.exec('SELECT namespace, COUNT(*) FROM memory_entries GROUP BY namespace');
89
101
  console.log('BEFORE:', JSON.stringify(before[0]?.values));
90
102
 
91
- // Purge a namespace (embeddings are inline — no separate table to clean)
92
103
  db.run(\"DELETE FROM memory_entries WHERE namespace = 'code-map'\");
93
104
 
94
105
  const after = db.exec('SELECT namespace, COUNT(*) FROM memory_entries GROUP BY namespace');
95
106
  console.log('AFTER:', JSON.stringify(after[0]?.values));
96
107
 
97
- writeFileSync('.moflo/moflo.db', Buffer.from(db.export()));
108
+ db.save();
98
109
  db.close();
99
110
  "
100
111
  ```
101
112
 
102
- > **Important sql.js write-back clobbers manual edits.** Long-lived processes (the moflo daemon, MCP servers) read the DB once on startup and dump the entire in-memory state on every flush. Direct `node -e` writes against `.moflo/moflo.db` while the daemon is running will be overwritten the next time it flushes. Run `flo daemon stop` (or stop your editor's MCP session) before purging, then restart afterwards.
113
+ Under node:sqlite the `db.save()` call is a no-op (WAL persists incrementally), but the factory keeps the API parity so the same snippet works against any backend the factory currently knows about. Concurrent writers serialize through WAL running this while the daemon is up no longer clobbers anything but if you want to be defensive, `flo daemon stop` before purging is still the cleanest sequence.
103
114
 
104
115
  After purging, reindex the namespace and rebuild embeddings:
105
116
  ```bash
@@ -91,6 +91,35 @@ When you find that the test is the actual problem: change the test, document why
91
91
 
92
92
  ---
93
93
 
94
+ ## Never Mask a Production Bug with Test-Side Workarounds
95
+
96
+ **When a test catches a real bug, fix the bug — never the test.** A test that passes by avoiding the broken path is a lie about coverage.
97
+
98
+ | Symptom test exposed | Right move | Wrong move (firing-offense pattern) |
99
+ |---------------------|-----------|-------------------------------------|
100
+ | Race between writers | Coordinate the writers | Reorder test so the race window closes before the assertion |
101
+ | Data corruption from a known writer | Find and fix the writer | Pre-seed the corrupted state so the assertion no longer fires |
102
+ | Stale cache served to readers | Invalidate the cache properly | Add `sleep` / `refresh()` in test to mask staleness |
103
+ | Cross-process clobber on a shared file | Single-writer ownership or atomic write | Order the test so only one writer runs |
104
+ | Missing precondition guard in prod | Add the guard | Have test setup satisfy the guard's precondition |
105
+
106
+ **Smell test:** read the test setup as a description of production behavior. If steps appear that production code does not perform, the test is mocking around a real bug.
107
+
108
+ ### Case Study: #1053 Memory Traversal
109
+
110
+ > "Healer: probeMemoryGetNeighbors() runs first in checkMemoryAccessFunctional so the bridge instantiates after the seed lands (sql.js whole-DB writeback clobbers external file writes once the in-memory snapshot warms)." — PR #1053 commit
111
+
112
+ Diagnosis correct; fix wrong. The clobber stayed in production. `moflo@4.9.37` shipped two regressions to every consumer (migration-induced embedding loss + `memory_store` silent drop) because the suite was engineered around the failure it had just detected.
113
+
114
+ ### Self-Check Before Merging Any Test-Only Change
115
+
116
+ 1. **Did the test fail first?** Name the production bug it caught.
117
+ 2. **Did my fix change only the test?** If yes, the runtime contract must be the bug — document why in the commit message.
118
+ 3. **Would a real user still hit this in production?** If yes, fix the bug, not the test.
119
+ 4. **Does test code comment *why* it sleeps / reorders / mocks?** That comment usually names a production bug — move the fix to production.
120
+
121
+ ---
122
+
94
123
  ## Concrete Example: #1017 Hive-Mind Shutdown
95
124
 
96
125
  This is the canonical case study for this guidance — and it has a second-order lesson that makes it even more useful.
@@ -141,11 +170,29 @@ Reviewers should reject — not just question — PRs that show patch-on-patch s
141
170
  | New fix adds a layer without removing one | Ask: "what was wrong with the prior layer? why does it stay?" |
142
171
  | Comment in new code says "for safety" or "just in case" | Ask: "what specific failure is this preventing? cite the line that produces it" |
143
172
  | The PR description says "this should fix the flake" without a deterministic repro | Ask: "what was the actual root cause? the writeup doesn't name it" |
173
+ | Commit message names a production bug, then describes test setup that avoids it | **Reject.** The bug stays live in production. Demand a fix at the production-code locus, not at the test. |
144
174
 
145
175
  These questions are not pedantic. They are the difference between fixing a bug and growing the surface area of bugs.
146
176
 
147
177
  ---
148
178
 
179
+ ## Scoping a Fix Issue — Kill the Class, Not the Instance
180
+
181
+ **When you file an issue for a bug, scope it to eliminate the bug class, not patch the visible instance.** Partial scope guarantees a follow-up epic.
182
+
183
+ | Posture | Right | Wrong |
184
+ |---------|-------|-------|
185
+ | Scope | Name the bug class; list every writer / caller / consumer that can produce it | Name only the surface where the symptom showed |
186
+ | Strategy | Commit to one approach in the body — "we will do X" | "Pick one of: option A or option B" |
187
+ | Audit | Enumerate every member of the class (every writer, every caller, every migration) | Patch the one path that broke today |
188
+ | Acceptance | Every original failure mode reproduces today and is gone after the epic; a new violator triggers a loud test | Symptom no longer reproduces |
189
+ | Tests | Mirror the production failure shape (multi-process, cross-writer, time-coupled) | In-process unit tests only |
190
+ | Follow-ups | None — anything that would be a follow-up is added to scope now | "We'll address this in a separate issue" |
191
+
192
+ If the epic body says "we'll figure out X in a follow-up", the scope is wrong — fold X in or explain why it cannot be in scope (e.g., depends on a release boundary).
193
+
194
+ ---
195
+
149
196
  ## How to Apply When You Are Stuck
150
197
 
151
198
  If you genuinely cannot find the root cause after stepping back:
@@ -18,6 +18,10 @@ CLI fallback when MCP is unavailable: `npx flo memory search --query "..." --nam
18
18
 
19
19
  The full namespace reference, query examples by domain, and tool catalog live in `.claude/guidance/moflo-agent-rules.md` § Memory-First Protocol — read that next.
20
20
 
21
+ ### Traverse, don't bulk-retrieve
22
+
23
+ Search hits carry a compact `navigation` crumb. For adjacent/sibling/hierarchical context, call `mcp__moflo__memory_get_neighbors` (one round-trip) instead of retrieving every hit. Full protocol: `.claude/guidance/moflo-memory-protocol.md`.
24
+
21
25
  ---
22
26
 
23
27
  ## Step 2: Apply Universal Agent Rules
@@ -307,7 +307,7 @@ switch (command) {
307
307
  process.stdout.write('REMINDER: Use TaskCreate before spawning agents. Task tool is blocked until then.\n');
308
308
  }
309
309
  if (config.memory_first && s.memoryRequired && !s.memorySearched) {
310
- process.stdout.write('REMINDER: Search memory (mcp__moflo__memory_search) before spawning agents.\n');
310
+ process.stdout.write('REMINDER: Search memory (mcp__moflo__memory_search) before spawning agents. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
311
311
  }
312
312
  if (s.lastNamespaceHint) {
313
313
  // Per-actor single-shot. Each session_id gets the hint at most once per
@@ -376,7 +376,7 @@ switch (command) {
376
376
  if (!s.memoryRequired || isMemorySearchedFor(s)) break;
377
377
  var target = (process.env.TOOL_INPUT_pattern || '') + ' ' + (process.env.TOOL_INPUT_path || '');
378
378
  if (EXEMPT.some(function(p) { return target.indexOf(p) >= 0; })) break;
379
- process.stderr.write('BLOCKED: Search memory before exploring files. Use mcp__moflo__memory_search.\n');
379
+ process.stderr.write('BLOCKED: Search memory before exploring files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
380
380
  process.exit(2);
381
381
  }
382
382
  case 'check-before-read': {
@@ -386,7 +386,7 @@ switch (command) {
386
386
  var fp = process.env.TOOL_INPUT_file_path || '';
387
387
  var isGuidance = fp.indexOf('.claude/guidance/') >= 0 || fp.indexOf('.claude\\guidance\\') >= 0;
388
388
  if (!isGuidance && EXEMPT.some(function(p) { return fp.indexOf(p) >= 0; })) break;
389
- process.stderr.write('BLOCKED: Search memory before reading files. Use mcp__moflo__memory_search.\n');
389
+ process.stderr.write('BLOCKED: Search memory before reading files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
390
390
  process.exit(2);
391
391
  }
392
392
  case 'record-task-created': {
@@ -485,58 +485,81 @@ function getHooksStatus() {
485
485
  }
486
486
 
487
487
  // Embeddings stats — reads from cache file written by embedding/memory ops.
488
- // No subprocess spawning. Falls back to DB file size estimate if cache is missing.
488
+ // No subprocess spawning.
489
+ //
490
+ // Reconciliation (epic #1054.S5 / #1059): the cached vectorCount is only
491
+ // trusted when (a) the daemon owns the lock and (b) the daemon's reported
492
+ // `version` matches the installed package version. Either failing means the
493
+ // cache may have been written by a clobbered or pre-upgrade writer, so the
494
+ // number is flagged `reconciled: false` and the render path shows `?` instead
495
+ // of a value that can't be vouched for. The prior `dbSize / 2` heuristic
496
+ // fallback was exactly the unreconciled fabrication the story prohibits, so
497
+ // it's removed.
489
498
  function getEmbeddingsStats() {
490
499
  let vectorCount = 0;
491
500
  let dbSizeKB = 0;
492
501
  let namespaces = 0;
493
502
  let hasHnsw = false;
503
+ let reconciled = false;
504
+ let staleReason = null;
494
505
 
495
- // Read cached stats (written by memory store/embed/rebuild commands).
496
- // `.moflo/` is the canonical location post-#699; the `.swarm/` parallel
497
- // write was retired in #714 because every writer (bridge-core,
498
- // memory-initializer, build-embeddings) now writes here, so the legacy
499
- // fallback can only ever be stale — exactly the divergence trap that
500
- // produced #639.
501
506
  const cached = readJSON(path.join(CWD, '.moflo', 'vector-stats.json'));
502
507
  if (cached && typeof cached.vectorCount === 'number') {
503
508
  vectorCount = cached.vectorCount;
504
509
  dbSizeKB = cached.dbSizeKB || 0;
505
510
  namespaces = cached.namespaces || 0;
506
511
  hasHnsw = cached.hasHnsw || false;
507
- return { vectorCount, dbSizeKB, namespaces, hasHnsw };
508
- }
509
512
 
510
- // Fallback: estimate from DB file size (no subprocess). Same probe order
511
- // as `getLearningStats` above see comment there.
512
- const dbFiles = [
513
- path.join(CWD, '.moflo', 'moflo.db'),
514
- path.join(CWD, '.swarm', 'memory.db'),
515
- path.join(CWD, 'data', 'memory.db'),
516
- path.join(CWD, '.claude', 'memory.db'),
517
- ];
518
- for (const f of dbFiles) {
519
- const stat = safeStat(f);
520
- if (stat) {
521
- dbSizeKB = Math.floor(stat.size / 1024);
522
- vectorCount = Math.floor(dbSizeKB / 2);
523
- namespaces = 1;
524
- break;
513
+ // Reconciliation against the daemon (single-writer authority post-#1054).
514
+ const daemonLock = readJSON(path.join(CWD, '.moflo', 'daemon.lock'));
515
+ const installedPkg = readJSON(path.join(CWD, 'node_modules', 'moflo', 'package.json'))
516
+ || readJSON(path.join(CWD, 'package.json'));
517
+ if (!daemonLock || typeof daemonLock.pid !== 'number') {
518
+ staleReason = 'daemon-down';
519
+ } else if (!installedPkg || typeof installedPkg.version !== 'string') {
520
+ staleReason = 'pkg-unknown';
521
+ } else if (daemonLock.version !== installedPkg.version) {
522
+ staleReason = 'version-skew';
523
+ } else {
524
+ reconciled = true;
525
525
  }
526
+ } else {
527
+ staleReason = 'cache-missing';
526
528
  }
527
529
 
528
530
  const hnswPaths = [
529
531
  path.join(CWD, '.moflo', 'hnsw.index'),
530
532
  path.join(CWD, '.swarm', 'hnsw.index'), // legacy pre-#727
531
533
  ];
532
- for (const p of hnswPaths) {
533
- if (safeStat(p)) {
534
- hasHnsw = true;
535
- break;
534
+ if (!hasHnsw) {
535
+ for (const p of hnswPaths) {
536
+ if (safeStat(p)) {
537
+ hasHnsw = true;
538
+ break;
539
+ }
540
+ }
541
+ }
542
+
543
+ // Fall back to a DB-size probe ONLY for the size display (which has no truth
544
+ // claim); vectorCount remains 0 when cache is missing rather than fabricating
545
+ // a divide-by-2 heuristic.
546
+ if (dbSizeKB === 0) {
547
+ const dbFiles = [
548
+ path.join(CWD, '.moflo', 'moflo.db'),
549
+ path.join(CWD, '.swarm', 'memory.db'),
550
+ path.join(CWD, 'data', 'memory.db'),
551
+ path.join(CWD, '.claude', 'memory.db'),
552
+ ];
553
+ for (const f of dbFiles) {
554
+ const stat = safeStat(f);
555
+ if (stat) {
556
+ dbSizeKB = Math.floor(stat.size / 1024);
557
+ break;
558
+ }
536
559
  }
537
560
  }
538
561
 
539
- return { vectorCount, dbSizeKB, namespaces, hasHnsw };
562
+ return { vectorCount, dbSizeKB, namespaces, hasHnsw, reconciled, staleReason };
540
563
  }
541
564
 
542
565
  // Test stats (count files only — NO reading file contents)
@@ -798,13 +821,20 @@ function generateDashboard() {
798
821
  // Embeddings line \u2014 vector store stats from .moflo/vector-stats.json.
799
822
  // Reuses `system.embeddings` (already computed by getSystemMetrics()) instead
800
823
  // of re-probing the cache file on every render.
824
+ //
825
+ // Reconciliation (#1054.S5 / #1059): when `vec.reconciled` is false, render
826
+ // the count as `?` and tag the reason \u2014 the underlying cache could have been
827
+ // written by a stale daemon and we refuse to display an unverified number.
801
828
  {
802
829
  const vec = system.embeddings;
803
- if (vec.vectorCount > 0) {
830
+ if (vec.vectorCount > 0 || !vec.reconciled) {
804
831
  const hnswInd = vec.hasHnsw ? `${c.brightGreen}\u26A1${c.reset}` : '';
805
832
  const sizeDisp = vec.dbSizeKB >= 1024 ? `${(vec.dbSizeKB / 1024).toFixed(1)}MB` : `${vec.dbSizeKB}KB`;
833
+ const countDisp = vec.reconciled
834
+ ? `${c.brightGreen}\u25CF${vec.vectorCount}${c.reset}${hnswInd}`
835
+ : `${c.brightYellow}?${c.reset} ${c.dim}(${vec.staleReason || 'unreconciled'})${c.reset}`;
806
836
  const eParts = [
807
- `${c.cyan}Vectors${c.reset} ${c.brightGreen}\u25CF${vec.vectorCount}${c.reset}${hnswInd}`,
837
+ `${c.cyan}Vectors${c.reset} ${countDisp}`,
808
838
  `${c.cyan}Size${c.reset} ${c.brightWhite}${sizeDisp}${c.reset}`,
809
839
  ];
810
840
  if (vec.namespaces > 0) {
@@ -877,13 +907,19 @@ function generateCompactDashboard() {
877
907
  }
878
908
  // Embeddings \u2014 always-on when vectorCount > 0; self-hides on a fresh install.
879
909
  // Compact doesn't call getSystemMetrics() so this is the only probe per render.
910
+ // Reconciliation: when the count is unreconciled (#1054.S5 / #1059), render
911
+ // `?` rather than displaying a value that can't be verified across the
912
+ // cache + daemon-reported state.
880
913
  {
881
914
  const vec = getEmbeddingsStats();
882
- if (vec.vectorCount > 0) {
915
+ if (vec.vectorCount > 0 || !vec.reconciled) {
883
916
  const hnswInd = vec.hasHnsw ? '\u26A1' : '';
884
917
  const sizeDisp = vec.dbSizeKB >= 1024 ? `${(vec.dbSizeKB / 1024).toFixed(1)}MB` : `${vec.dbSizeKB}KB`;
918
+ const countDisp = vec.reconciled
919
+ ? `${c.brightGreen}${vec.vectorCount}${hnswInd}${c.reset}`
920
+ : `${c.brightYellow}?${c.reset}`;
885
921
  segments.push(
886
- `${c.brightCyan}\uD83D\uDCCA${c.reset} ${c.brightGreen}${vec.vectorCount}${hnswInd}${c.reset} ${c.dim}(${sizeDisp})${c.reset}`
922
+ `${c.brightCyan}\uD83D\uDCCA${c.reset} ${countDisp} ${c.dim}(${sizeDisp})${c.reset}`
887
923
  );
888
924
  }
889
925
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "directive": "MANDATORY FIRST ACTION: Your very first tool call MUST be mcp__moflo__memory_search (any query, any namespace). The memory-first gate WILL BLOCK all Glob, Grep, and Read calls until you do this. After memory search, follow `.claude/guidance/moflo-subagents.md` protocol."
2
+ "directive": "MANDATORY FIRST ACTION: Your very first tool call MUST be mcp__moflo__memory_search (any query, any namespace). The memory-first gate WILL BLOCK all Glob, Grep, and Read calls until you do this. After memory search, follow `.claude/guidance/moflo-subagents.md` protocol. When a search hit carries `navigation`, you MUST call mcp__moflo__memory_get_neighbors to traverse — calling mcp__moflo__memory_retrieve on every hit is a protocol violation. See `.claude/guidance/moflo-memory-protocol.md`."
3
3
  }
@@ -22,7 +22,7 @@ const path = require('path');
22
22
  // Defense-in-depth copy of the canonical directive in subagent-bootstrap.json.
23
23
  // Kept as a single-line literal so the parity test in tests/bin/subagent-start.test.ts
24
24
  // can verify it matches the JSON via plain substring containment.
25
- const FALLBACK_DIRECTIVE = 'MANDATORY FIRST ACTION: Your very first tool call MUST be mcp__moflo__memory_search (any query, any namespace). The memory-first gate WILL BLOCK all Glob, Grep, and Read calls until you do this. After memory search, follow `.claude/guidance/moflo-subagents.md` protocol.';
25
+ const FALLBACK_DIRECTIVE = 'MANDATORY FIRST ACTION: Your very first tool call MUST be mcp__moflo__memory_search (any query, any namespace). The memory-first gate WILL BLOCK all Glob, Grep, and Read calls until you do this. After memory search, follow `.claude/guidance/moflo-subagents.md` protocol. When a search hit carries `navigation`, you MUST call mcp__moflo__memory_get_neighbors to traverse — calling mcp__moflo__memory_retrieve on every hit is a protocol violation. See `.claude/guidance/moflo-memory-protocol.md`.';
26
26
 
27
27
  function loadDirective() {
28
28
  const jsonPath = path.join(__dirname, 'subagent-bootstrap.json');
@@ -121,6 +121,14 @@ mcp__moflo__memory_stats — { namespace: "learnings" }
121
121
 
122
122
  Flag empty `learnings` as `info` (project hasn't accumulated decisions yet — fine for new projects). Flag empty `guidance` as `warn` (no indexed guidance means semantic search is degraded).
123
123
 
124
+ **Legacy `doc-*` residue (#1053 S4)** — moflo retired whole-document indexing in favor of chunk-only RAG. The `purge-doc-entries` migration runs on session-start; if any `doc-*` rows linger, the migration didn't fire (ran with no DB, errored, or the install is below the migration's introduction).
125
+
126
+ ```
127
+ mcp__moflo__memory_search — { query: "doc-", namespace: "guidance", threshold: 0, limit: 5 }
128
+ ```
129
+
130
+ If any returned `key` starts with `doc-`, flag `info`: "legacy doc-* rows present — `purge-doc-entries` migration did not run; fixable via `flo healer --fix` or manual `node node_modules/moflo/bin/run-migrations.mjs`".
131
+
124
132
  ### 1i. Hooks & MCP Wiring
125
133
 
126
134
  Read `.claude/settings.json`. Check:
@@ -16,27 +16,16 @@
16
16
  * flo-embeddings --namespace guidance # scope to one namespace
17
17
  */
18
18
 
19
- import { existsSync, readFileSync, writeFileSync } from 'fs';
20
- import { resolve, dirname } from 'path';
21
- import { mofloResolveURL, mofloInternalURL } from './lib/moflo-resolve.mjs';
22
- import { memoryDbPath, hnswIndexPath } from './lib/moflo-paths.mjs';
23
- const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
19
+ import { existsSync, readFileSync } from 'fs';
20
+ import { mofloInternalURL } from './lib/moflo-resolve.mjs';
21
+ import { memoryDbPath, hnswIndexPath, findProjectRoot } from './lib/moflo-paths.mjs';
22
+ import { openBackend } from './lib/get-backend.mjs';
24
23
  const FASTEMBED_INLINE = 'dist/src/cli/embeddings/fastembed-inline/index.js';
25
24
  const BRIDGE_CORE = 'dist/src/cli/memory/bridge-core.js';
26
25
  const HNSW_PERSISTENCE = 'dist/src/cli/memory/hnsw-persistence.js';
27
26
  const { writeVectorStatsJson } = await import(mofloInternalURL(BRIDGE_CORE));
28
27
  const { buildAndWriteHnswSidecar } = await import(mofloInternalURL(HNSW_PERSISTENCE));
29
28
 
30
- function findProjectRoot() {
31
- let dir = process.cwd();
32
- const root = resolve(dir, '/');
33
- while (dir !== root) {
34
- if (existsSync(resolve(dir, 'package.json'))) return dir;
35
- dir = dirname(dir);
36
- }
37
- return process.cwd();
38
- }
39
-
40
29
  const projectRoot = findProjectRoot();
41
30
  const DB_PATH = memoryDbPath(projectRoot);
42
31
 
@@ -100,14 +89,11 @@ async function getDb() {
100
89
  if (!existsSync(DB_PATH)) {
101
90
  throw new Error(`Database not found: ${DB_PATH}`);
102
91
  }
103
- const SQL = await initSqlJs();
104
- const buffer = readFileSync(DB_PATH);
105
- return new SQL.Database(buffer);
92
+ return openBackend(projectRoot, { create: false });
106
93
  }
107
94
 
108
95
  function saveDb(db) {
109
- const data = db.export();
110
- writeFileSync(DB_PATH, Buffer.from(data));
96
+ db.save();
111
97
  }
112
98
 
113
99
  function getEntriesNeedingEmbeddings(db, namespace, forceAll) {
package/bin/cli.js CHANGED
@@ -13,6 +13,11 @@
13
13
  // already ships with correct .js extensions — the patch is unnecessary.
14
14
  process.env.SKIP_AGENTDB_PATCH ??= '1';
15
15
 
16
+ // MUST run before any `node:sqlite` import in the process tree so the
17
+ // once-per-process ExperimentalWarning never fires. See module header for
18
+ // why we filter rather than `--no-warnings`-style broadly suppressing.
19
+ import './lib/suppress-sqlite-warning.mjs';
20
+
16
21
  import { randomUUID } from 'crypto';
17
22
 
18
23
  // Check if we should run in MCP server mode
package/bin/gate.cjs CHANGED
@@ -307,7 +307,7 @@ switch (command) {
307
307
  process.stdout.write('REMINDER: Use TaskCreate before spawning agents. Task tool is blocked until then.\n');
308
308
  }
309
309
  if (config.memory_first && s.memoryRequired && !s.memorySearched) {
310
- process.stdout.write('REMINDER: Search memory (mcp__moflo__memory_search) before spawning agents.\n');
310
+ process.stdout.write('REMINDER: Search memory (mcp__moflo__memory_search) before spawning agents. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
311
311
  }
312
312
  if (s.lastNamespaceHint) {
313
313
  // Per-actor single-shot. Each session_id gets the hint at most once per
@@ -376,7 +376,7 @@ switch (command) {
376
376
  if (!s.memoryRequired || isMemorySearchedFor(s)) break;
377
377
  var target = (process.env.TOOL_INPUT_pattern || '') + ' ' + (process.env.TOOL_INPUT_path || '');
378
378
  if (EXEMPT.some(function(p) { return target.indexOf(p) >= 0; })) break;
379
- process.stderr.write('BLOCKED: Search memory before exploring files. Use mcp__moflo__memory_search.\n');
379
+ process.stderr.write('BLOCKED: Search memory before exploring files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
380
380
  process.exit(2);
381
381
  }
382
382
  case 'check-before-read': {
@@ -386,7 +386,7 @@ switch (command) {
386
386
  var fp = process.env.TOOL_INPUT_file_path || '';
387
387
  var isGuidance = fp.indexOf('.claude/guidance/') >= 0 || fp.indexOf('.claude\\guidance\\') >= 0;
388
388
  if (!isGuidance && EXEMPT.some(function(p) { return fp.indexOf(p) >= 0; })) break;
389
- process.stderr.write('BLOCKED: Search memory before reading files. Use mcp__moflo__memory_search.\n');
389
+ process.stderr.write('BLOCKED: Search memory before reading files. Use mcp__moflo__memory_search. On chunk hits, traverse via mcp__moflo__memory_get_neighbors — see .claude/guidance/moflo-memory-protocol.md\n');
390
390
  process.exit(2);
391
391
  }
392
392
  case 'record-task-created': {
@@ -29,26 +29,14 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from
29
29
  import { resolve, dirname, relative, basename, extname } from 'path';
30
30
  import { fileURLToPath } from 'url';
31
31
  import { execSync, execFileSync, spawn } from 'child_process';
32
- import { mofloResolveURL } from './lib/moflo-resolve.mjs';
33
- import { memoryDbPath, MOFLO_DIR } from './lib/moflo-paths.mjs';
32
+ import { memoryDbPath, MOFLO_DIR, findProjectRoot } from './lib/moflo-paths.mjs';
33
+ import { openBackend } from './lib/get-backend.mjs';
34
34
  import { applyIncrementalChunks, computeContentListHash } from './lib/incremental-write.mjs';
35
35
  import { resolveMofloBin } from './lib/resolve-bin.mjs';
36
- const initSqlJs = (await import(mofloResolveURL('sql.js'))).default;
37
36
 
38
37
 
39
38
  const __dirname = dirname(fileURLToPath(import.meta.url));
40
39
 
41
- // Detect project root: walk up from cwd to find a package.json
42
- function findProjectRoot() {
43
- let dir = process.cwd();
44
- const root = resolve(dir, '/');
45
- while (dir !== root) {
46
- if (existsSync(resolve(dir, 'package.json'))) return dir;
47
- dir = dirname(dir);
48
- }
49
- return process.cwd();
50
- }
51
-
52
40
  const projectRoot = findProjectRoot();
53
41
  const NAMESPACE = 'code-map';
54
42
  const DB_PATH = memoryDbPath(projectRoot);
@@ -110,14 +98,7 @@ function ensureDbDir() {
110
98
 
111
99
  async function getDb() {
112
100
  ensureDbDir();
113
- const SQL = await initSqlJs();
114
- let db;
115
- if (existsSync(DB_PATH)) {
116
- const buffer = readFileSync(DB_PATH);
117
- db = new SQL.Database(buffer);
118
- } else {
119
- db = new SQL.Database();
120
- }
101
+ const db = await openBackend(projectRoot, { create: true });
121
102
 
122
103
  db.run(`
123
104
  CREATE TABLE IF NOT EXISTS memory_entries (
@@ -147,8 +128,7 @@ async function getDb() {
147
128
  }
148
129
 
149
130
  function saveDb(db) {
150
- const data = db.export();
151
- writeFileSync(DB_PATH, Buffer.from(data));
131
+ db.save();
152
132
  }
153
133
 
154
134
  function countNamespace(db) {
package/bin/hooks.mjs CHANGED
@@ -26,24 +26,15 @@ import { fileURLToPath, pathToFileURL } from 'url';
26
26
  import { createProcessManager } from './lib/process-manager.mjs';
27
27
  import { shouldDaemonAutoStart } from './lib/daemon-config.mjs';
28
28
  import { resolveMofloBin } from './lib/resolve-bin.mjs';
29
+ import { findProjectRoot } from './lib/moflo-paths.mjs';
29
30
 
30
31
  const __filename = fileURLToPath(import.meta.url);
31
32
  const __dirname = dirname(__filename);
32
33
 
33
- // Detect project root by walking up from cwd to find package.json.
34
34
  // IMPORTANT: Do NOT use resolve(__dirname, '..') or '../..' — this script lives
35
35
  // in bin/ during development but gets synced to .claude/scripts/ in consumer
36
- // projects, so __dirname-relative paths break. findProjectRoot() works everywhere.
37
- function findProjectRoot() {
38
- let dir = process.cwd();
39
- const root = resolve(dir, '/');
40
- while (dir !== root) {
41
- if (existsSync(resolve(dir, 'package.json'))) return dir;
42
- dir = dirname(dir);
43
- }
44
- return process.cwd();
45
- }
46
-
36
+ // projects, so __dirname-relative paths break. findProjectRoot() works
37
+ // everywhere and resolves identically to the TS bridge (see lib/moflo-paths.mjs).
47
38
  const projectRoot = findProjectRoot();
48
39
  const logFile = resolve(projectRoot, '.swarm/hooks.log');
49
40
  const pm = createProcessManager(projectRoot);
package/bin/index-all.mjs CHANGED
@@ -18,7 +18,7 @@ import { resolve, dirname } from 'path';
18
18
  import { fileURLToPath } from 'url';
19
19
  import { spawn, spawnSync } from 'child_process';
20
20
  import { platform } from 'os';
21
- import { hnswIndexPath } from './lib/moflo-paths.mjs';
21
+ import { hnswIndexPath, findProjectRoot } from './lib/moflo-paths.mjs';
22
22
  import {
23
23
  decideStepGate,
24
24
  computeStepFingerprint,
@@ -38,20 +38,10 @@ const ONNX_THREAD_CAP = {
38
38
 
39
39
  const __dirname = dirname(fileURLToPath(import.meta.url));
40
40
 
41
- // Detect project root by walking up from cwd to find package.json.
42
41
  // IMPORTANT: Do NOT use resolve(__dirname, '..') — this script lives in bin/
43
42
  // during development but gets synced to .claude/scripts/ in consumer projects,
44
- // so __dirname-relative paths break. findProjectRoot() works in both locations.
45
- function findProjectRoot() {
46
- let dir = process.cwd();
47
- const root = resolve(dir, '/');
48
- while (dir !== root) {
49
- if (existsSync(resolve(dir, 'package.json'))) return dir;
50
- dir = dirname(dir);
51
- }
52
- return process.cwd();
53
- }
54
-
43
+ // so __dirname-relative paths break. findProjectRoot() (lib/moflo-paths.mjs)
44
+ // works in both locations and resolves identically to the TS bridge.
55
45
  const projectRoot = findProjectRoot();
56
46
  const LOG_PATH = resolve(projectRoot, '.swarm/hooks.log');
57
47