moflo 4.10.28 → 4.10.29-rc.2

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.
@@ -189,3 +189,4 @@ findings; if AI-driven security scanning returns it should be an opt-in
189
189
  - `.claude/guidance/moflo-subagents.md` — Subagent memory-first protocol and store-back rules
190
190
  - `.claude/guidance/moflo-memory-strategy.md` — Memory namespaces, RAG linking, and search query patterns
191
191
  - `.claude/guidance/moflo-memorydb-maintenance.md` — How the namespaces above are populated and refreshed
192
+ - `.claude/guidance/moflo-cross-install-memory-sharing.md` — `flo memory sync` / `team-export` / `team-import` and `flo doctor -c shared-db` for sharing durable learnings across worktrees, machines, and teams
@@ -222,6 +222,7 @@ See `.claude/guidance/moflo-memory-strategy.md` for memory-specific troubleshoot
222
222
  - `.claude/guidance/moflo-claude-swarm-cohesion.md` — Task & swarm coordination with TaskCreate/TaskUpdate
223
223
  - `.claude/guidance/moflo-memory-strategy.md` — Database schema, namespaces, search commands, RAG linking
224
224
  - `.claude/guidance/moflo-memorydb-maintenance.md` — Reindexing, namespace purging, recovery
225
+ - `.claude/guidance/moflo-cross-install-memory-sharing.md` — Sharing durable `learnings` across worktrees / machines / teams (`memory.durable_path`, `flo memory sync`, the team artifact) and the `flo doctor -c shared-db` check
225
226
  - `.claude/guidance/moflo-settings-injection.md` — What moflo writes into `.claude/` and how surgical self-heal works
226
227
  - `.claude/guidance/moflo-cross-platform.md` — Windows/macOS/Linux portability rules for any code change
227
228
  - `.claude/guidance/moflo-verbose-command-filtering.md` — Filter long verbose commands at the source; never tee-then-grep
@@ -0,0 +1,126 @@
1
+ # Cross-Installation Memory Sharing
2
+
3
+ **Purpose:** Decide how to share moflo's durable learnings across git worktrees, machines, or a team — and how to do it WITHOUT corrupting search. Read this before configuring `memory.durable_path`, running `flo memory sync`, or pointing two checkouts at one database.
4
+
5
+ ---
6
+
7
+ ## Share the Durable Slice, Never the Whole DB
8
+
9
+ moflo's `.moflo/moflo.db` holds three classes of data. Only one is worth sharing.
10
+
11
+ | Class | Namespaces | Shareable? | Why |
12
+ |-------|-----------|------------|-----|
13
+ | **Durable** | `learnings`, `knowledge` | **Yes** | User-authored, slow to recreate, identical across checkouts |
14
+ | **Structural** | `code-map`, guidance chunks | No | Branch-specific; rebuilds cheaply from the indexers |
15
+ | **Ephemeral** | `tasklist`, `hive-mind`, epic state | No | Per-run scratch; meaningless in another install |
16
+
17
+ **Never *live-share* the entire `moflo.db` between two running daemons.** node:sqlite + WAL stops concurrent writers from losing rows, but each daemon builds its own in-memory HNSW index — a write in install A never updates install B's index, so search silently returns stale results, and structural namespaces churn across branches. For ongoing sync, share ONLY the durable slice.
18
+
19
+ > **Not the same thing:** restoring a one-time whole-DB *snapshot* to seed a fresh/empty workspace is safe and fast — each workspace then owns its own copy (no second concurrent daemon, so no index divergence), and the incremental reindex reconciles branch drift. That avoids the cold-start full parse + re-embed and is the right tool when speed-to-ready matters. Use `flo memory backup`/`restore` for it — see "Hydrate a Fresh Workspace from a Snapshot" below. The rule above is specifically about *live concurrent* sharing.
20
+
21
+ ---
22
+
23
+ ## Pick the Mechanism by Topology
24
+
25
+ Choose by who needs the learnings, not by what is easiest to wire.
26
+
27
+ | Topology | Mechanism | How |
28
+ |----------|-----------|-----|
29
+ | Same machine, many worktrees / Conductor workspaces | `memory.durable_path` | Point every checkout at one durable-only store; session-start seeds + writes through automatically |
30
+ | One person, many machines | `flo memory sync` | `--to <file>` then `--from <file>` via a synced folder (Dropbox/iCloud) or a hand-copied file |
31
+ | A team sharing one repo | Git-tracked team artifact | `flo memory team-export` writes `.moflo/shared/learnings.jsonl`; teammates' session-start import-merges it after `git pull` |
32
+ | A fresh/empty workspace that must be ready FAST | Whole-DB snapshot (`memory.hydrate_from`) | `flo memory backup --to <snap>` once; a new workspace restores the entire DB so search works on session one — no cold reindex |
33
+
34
+ The first three move the SAME durable slice and dedupe on `UNIQUE(namespace, key)`, so combining them is conflict-free. The snapshot is a different tool — a one-time whole-DB seed that composes with the durable-slice modes (see the next section but one).
35
+
36
+ ---
37
+
38
+ ## Configure a Same-Machine Shared Store
39
+
40
+ Set `memory.durable_path` in `moflo.yaml` to a dedicated file — NOT a full `moflo.db`.
41
+
42
+ ```yaml
43
+ memory:
44
+ durable_path: ~/.moflo-shared/team-learnings.db # a durable-only store, created on first flush
45
+ ```
46
+
47
+ The path must resolve to a file that is NOT any project's `.moflo/moflo.db`. Pointing it at a real `moflo.db` re-introduces the whole-DB-sharing hazard. `MOFLO_DURABLE_PATH` overrides the config per process; relative paths resolve against the project root.
48
+
49
+ ---
50
+
51
+ ## Carry Learnings Between Your Own Machines
52
+
53
+ Use `flo memory sync` with a portable artifact you keep in a synced folder.
54
+
55
+ ```bash
56
+ flo memory sync --to ~/Dropbox/moflo/durable.db # on machine A
57
+ flo memory sync --from ~/Dropbox/moflo/durable.db # on machine B
58
+ ```
59
+
60
+ `--from` is idempotent (re-running copies nothing new). After an import, restart the Claude Code session (or run `flo memory rebuild-index`) so the merged rows are embedded and searchable.
61
+
62
+ ---
63
+
64
+ ## Share Learnings Across a Team
65
+
66
+ Commit a git-tracked JSONL artifact so the whole team accumulates each other's learnings about the repo.
67
+
68
+ ```bash
69
+ flo memory team-export # writes .moflo/shared/learnings.jsonl + makes it git-trackable
70
+ git add .moflo/shared/learnings.jsonl && git commit -m "share learnings"
71
+ ```
72
+
73
+ Teammates' session-start import-merges the file after `git pull` (first-write-wins on conflicts; author/host provenance is retained). JSONL keeps git diffs reviewable; embeddings are regenerated on import. Enable it with `memory.team_artifact: .moflo/shared/learnings.jsonl`.
74
+
75
+ ---
76
+
77
+ ## Hydrate a Fresh Workspace from a Snapshot
78
+
79
+ Use a whole-DB snapshot when a new workspace must be searchable on its FIRST session. An empty `.moflo/moflo.db` forces a full cold reindex — parse every file for `code-map`/`patterns`/`tests`, chunk all `guidance`, AND run ONNX embedding generation over all of it (minutes on a large repo). A snapshot restore skips both the parse and the embed.
80
+
81
+ ```bash
82
+ flo memory backup --to ~/moflo-snapshots/myproject.db # snapshot a warm workspace (VACUUM INTO — consistent even under an active WAL)
83
+ flo memory restore --from ~/moflo-snapshots/myproject.db # seed a fresh workspace (no-op if the local DB already has content; --force to override)
84
+ ```
85
+
86
+ Auto-hydrate on session-start by setting `memory.hydrate_from` (env `MOFLO_HYDRATE_FROM` overrides):
87
+
88
+ ```yaml
89
+ memory:
90
+ hydrate_from: /abs/path/to/moflo-snapshot.db # restored only when the local DB is empty, before the daemon starts
91
+ ```
92
+
93
+ | Behavior | Rule |
94
+ |----------|------|
95
+ | When it restores | Only when the local DB is absent/empty (never clobbers an active workspace unless `--force`) |
96
+ | Ephemeral leak | Source `tasklist`/`hive-mind`/epic-state are purged from the restored copy |
97
+ | Branch drift | Self-heals on the next incremental reindex |
98
+ | Order vs. durable-slice | Hydrate runs BEFORE the durable-slice sync at session-start — snapshot is the fast one-time seed, durable-slice keeps `learnings` converged afterward |
99
+
100
+ This is NOT whole-DB *live* sharing: each restored workspace owns its own copy, so there is no second concurrent daemon and no HNSW index divergence.
101
+
102
+ ---
103
+
104
+ ## Verify the Setup with `flo doctor`
105
+
106
+ Run the shared-DB check to confirm you are sharing safely.
107
+
108
+ ```bash
109
+ flo doctor -c shared-db
110
+ ```
111
+
112
+ | Result | Meaning |
113
+ |--------|---------|
114
+ | **pass** — "durable-only shared store configured" | `durable_path` points at a dedicated file — safe |
115
+ | **warn** — "points at a full moflo.db" | `durable_path` aliases or is named like a full DB — repoint it at a dedicated store |
116
+ | **warn** — "is a symlink" | The local `moflo.db` is symlinked into a shared location — replace it with a local DB and use `durable_path` |
117
+
118
+ A foreign-writer warning from the Writers Audit check (`flo doctor -c writers`) means another process is writing the DB outside the daemon's single-writer routing; under node:sqlite + WAL it won't lose rows, but it leaves the daemon's HNSW index stale until it reindexes.
119
+
120
+ ---
121
+
122
+ ## See Also
123
+
124
+ - `.claude/guidance/moflo-memory-strategy.md` — Namespaces, RAG indexing, and the durable-vs-derived split this doc builds on
125
+ - `.claude/guidance/moflo-memory-protocol.md` — Search-and-traverse protocol for the shared `learnings` once it is populated
126
+ - `.claude/guidance/moflo-core-guidance.md` — CLI, daemon, and `moflo.yaml` reference (the `memory` config block)
@@ -152,6 +152,7 @@ The retrieval-specific rules below only apply to docs you want surfaced via `mem
152
152
  ## See Also
153
153
 
154
154
  - `.claude/guidance/moflo-memorydb-maintenance.md` — Database location, schema, purge/reindex procedures
155
+ - `.claude/guidance/moflo-cross-install-memory-sharing.md` — Sharing the durable `learnings` slice across worktrees, machines, and teams (and why never to share the whole DB)
155
156
  - `.claude/guidance/moflo-guidance-rules.md` — Universal authoring rules every guidance doc must follow
156
157
  - `.claude/guidance/moflo-subagents.md` — Memory-first subagent protocol that uses these search paths
157
158
  - `.claude/guidance/moflo-claude-swarm-cohesion.md` — Task & swarm coordination layered on top of memory
@@ -60,6 +60,9 @@ memory:
60
60
  backend: node-sqlite # node-sqlite (default) | rvf (pure-TS fallback) | json (last resort). Passed to createDatabase() as the preferred provider (#1144).
61
61
  embedding_model: Xenova/all-MiniLM-L6-v2 # 384-dim neural embeddings
62
62
  namespace: default # Default namespace for memory operations
63
+ # durable_path: ~/.moflo-shared/team-learnings.db # opt-in (#1232): share durable namespaces (learnings, knowledge) across worktrees / Conductor workspaces. Point at a DEDICATED store, never a full moflo.db. Env override: MOFLO_DURABLE_PATH.
64
+ # team_artifact: .moflo/shared/learnings.jsonl # opt-in (#1234): git-tracked JSONL the team commits; session-start import-merges it. Env override: MOFLO_TEAM_ARTIFACT. See moflo-cross-install-memory-sharing.md.
65
+ # hydrate_from: /abs/path/to/moflo-snapshot.db # opt-in (#1244): whole-DB snapshot to seed a fresh/empty workspace on session-start (skips the cold reindex). No-op once the local DB has content. Take one with "flo memory backup --to <path>". Env override: MOFLO_HYDRATE_FROM.
63
66
 
64
67
  # Hook toggles (all on by default — disable to slim down)
65
68
  hooks:
package/README.md CHANGED
@@ -67,6 +67,7 @@ MoFlo makes deliberate choices so you don't have to:
67
67
  |---------|-------------|
68
68
  | **Semantic Memory** | 384-dim domain-aware embeddings. Store knowledge, search it instantly. |
69
69
  | **Reflection** | Distills durable lessons from your sessions into searchable memory — automatically (auto-meditate), or on demand with `/meditate`. |
70
+ | **Cross-Install Sharing** | Share durable `learnings` across git worktrees, your own machines, or a whole team — via `memory.durable_path`, `flo memory sync`, or a git-tracked artifact. Structural namespaces stay local. [Full documentation →](.claude/guidance/shipped/moflo-cross-install-memory-sharing.md) |
70
71
  | **Code Navigation** | Indexes your codebase structure so Claude can answer "where does X live?" without Glob/Grep. |
71
72
  | **Guidance Indexing** | Chunks your project docs (`.claude/guidance/`, `docs/`) and makes them searchable. |
72
73
  | **Gates** | Enforces memory-first and task-creation patterns via Claude Code hooks. Prevents Claude from skipping steps. |
@@ -262,6 +263,21 @@ auto_meditate:
262
263
 
263
264
  The two are complementary: auto-meditate is the always-on safety net, `/meditate` is the deliberate, curated pass. Both write to the same `learnings` namespace and both deduplicate, so neither pollutes the other — and anything they store is available to semantic search from then on.
264
265
 
266
+ ### Sharing learnings across installations
267
+
268
+ The `learnings` (and `knowledge`) namespaces are the **durable** slice of memory — user-authored and worth carrying across checkouts. The rest of the DB (code-map, guidance chunks, run state) is structural or ephemeral and rebuilds cheaply, so it stays local. For ongoing sync MoFlo shares only the durable slice — **never *live-shares* the whole `moflo.db`** between two daemons, which would make each daemon's in-memory search index diverge.
269
+
270
+ Pick the mechanism by topology:
271
+
272
+ | You want to share across… | Use | How |
273
+ |---------------------------|-----|-----|
274
+ | Git worktrees / Conductor workspaces (same machine) | `memory.durable_path` in `moflo.yaml` | Point every checkout at one durable-only store; session-start seeds + writes through automatically |
275
+ | Your own laptop + desktop + CI | `flo memory sync --to/--from <file>` | Export to a synced folder (Dropbox/iCloud) or a copied file, import on the other machine |
276
+ | A whole team on one repo | `flo memory team-export` → commit `.moflo/shared/learnings.jsonl` | Teammates' session-start import-merges it after `git pull` (first-write-wins; author provenance retained) |
277
+ | A fresh workspace that must be ready fast | `flo memory backup/restore` or `memory.hydrate_from` | Seed a new workspace from a **whole-DB snapshot** so it's searchable on session one — no cold reindex. Safe because each workspace owns its own copy (a one-time restore, not live sharing) |
278
+
279
+ Verify your setup with `flo doctor -c shared-db` (it warns if you've accidentally pointed at a full `moflo.db` for *live* sharing). Full guide: [`moflo-cross-install-memory-sharing.md`](.claude/guidance/shipped/moflo-cross-install-memory-sharing.md).
280
+
265
281
  ## The Gate System
266
282
 
267
283
  MoFlo installs Claude Code hooks that run on every tool call. Together, these gates create a **feedback loop** that prevents Claude from wasting tokens on blind exploration and ensures it builds on prior knowledge.
@@ -247,7 +247,17 @@ function readInstallManifest(manifestPath) {
247
247
  return { entries, isLegacy };
248
248
  }
249
249
 
250
- const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
250
+ const plural = (n, word) => {
251
+ if (n === 1) return `${n} ${word}`;
252
+ // Pluralize the final word: consonant+y → -ies (entry → entries), sibilant
253
+ // endings → -es (box → boxes), otherwise → -s. Naive +s mangled every
254
+ // 'entry'-style word into 'entrys'.
255
+ let suffixed;
256
+ if (/[^aeiou]y$/i.test(word)) suffixed = `${word.slice(0, -1)}ies`;
257
+ else if (/(?:s|x|z|ch|sh)$/i.test(word)) suffixed = `${word}es`;
258
+ else suffixed = `${word}s`;
259
+ return `${n} ${suffixed}`;
260
+ };
251
261
 
252
262
  // Captured inside the upgrade/drift branch so the post-spawn notice writer
253
263
  // can persist `.moflo/upgrade-notice.json` for the statusline (#636).
@@ -2037,6 +2047,52 @@ try {
2037
2047
  } catch { /* writing the failure itself must not throw */ }
2038
2048
  }
2039
2049
 
2050
+ // ── 3e-1244. Hydrate a fresh workspace from a whole-DB snapshot BEFORE daemon ─
2051
+ // When `memory.hydrate_from` (or MOFLO_HYDRATE_FROM) points at a snapshot AND
2052
+ // the local .moflo/moflo.db is absent/empty, restore the whole DB (structural +
2053
+ // durable + embeddings) so the first session is searchable without a cold full
2054
+ // reindex (#1244, epic #1231). Runs BEFORE the daemon spawns so the restored
2055
+ // rows are present when the daemon builds its HNSW index, and BEFORE the purge/
2056
+ // sync blocks below so they operate on the hydrated DB. No-clobber: a no-op once
2057
+ // the local DB has content. Best-effort: a failure must never block session start.
2058
+ try {
2059
+ // Cheap pre-gate so the overwhelming majority (no hydrate configured) pay
2060
+ // nothing — no module load, no moflo.yaml parse. Live only when the env
2061
+ // override is set or moflo.yaml has an UNcommented `hydrate_from:` line (the
2062
+ // generated config ships a commented example, which this regex ignores).
2063
+ const hydrateYamlPath = resolve(projectRoot, 'moflo.yaml');
2064
+ let hydrateEnabled = Boolean(process.env.MOFLO_HYDRATE_FROM);
2065
+ if (!hydrateEnabled && existsSync(hydrateYamlPath)) {
2066
+ try {
2067
+ hydrateEnabled = /^[ \t]*hydrate_from[ \t]*:/m.test(readFileSync(hydrateYamlPath, 'utf-8'));
2068
+ } catch { /* unreadable yaml — treat as not-configured */ }
2069
+ }
2070
+ const hydratePaths = hydrateEnabled
2071
+ ? [
2072
+ resolve(projectRoot, 'node_modules/moflo/dist/src/cli/services/snapshot-restore.js'),
2073
+ resolve(projectRoot, 'dist/src/cli/services/snapshot-restore.js'),
2074
+ ]
2075
+ : [];
2076
+ const hydratePath = hydratePaths.find((p) => existsSync(p));
2077
+ if (hydratePath) {
2078
+ const { hydrateAtSessionStart } = await import(pathToFileURL(hydratePath).href);
2079
+ const result = await hydrateAtSessionStart({ projectRoot });
2080
+ if (result?.restored) {
2081
+ const mb = ((result.bytes ?? 0) / (1024 * 1024)).toFixed(1);
2082
+ emitMutation(
2083
+ 'hydrated workspace from snapshot',
2084
+ `restored ${mb} MB whole-DB snapshot — search ready without a cold reindex`,
2085
+ );
2086
+ }
2087
+ }
2088
+ } catch (err) {
2089
+ // Non-fatal — without a snapshot the workspace just cold-indexes as before.
2090
+ try {
2091
+ const msg = err && err.message ? err.message : String(err);
2092
+ process.stderr.write(`snapshot hydration skipped: ${msg}\n`);
2093
+ } catch { /* writing the failure itself must not throw */ }
2094
+ }
2095
+
2040
2096
  // ── 3e-728. Hard-delete leftover soft-delete tombstones (#728) ─────────────
2041
2097
  // Soft-delete was retired in story #728 — `status='deleted'` rows are now
2042
2098
  // unrecoverable bloat from prior moflo versions. Purge any stragglers and
@@ -2106,6 +2162,56 @@ try {
2106
2162
  } catch { /* writing the failure itself must not throw */ }
2107
2163
  }
2108
2164
 
2165
+ // ── 3e-1232. Sync durable namespaces with the shared store BEFORE daemon ────
2166
+ // When `memory.durable_path` (or MOFLO_DURABLE_PATH) is set, flush this
2167
+ // worktree's learnings to the shared store and seed any sibling-workspace
2168
+ // learnings back into the local DB. Runs BEFORE the daemon spawns so the
2169
+ // freshly-seeded rows are present when the daemon builds its in-memory HNSW
2170
+ // index — that's what makes the cross-worktree repro (#1231) pass. No-op when
2171
+ // the feature is off. Best-effort: a sync failure must never block session start.
2172
+ try {
2173
+ // Cheap pre-gate so solo users (the overwhelming majority) pay nothing here —
2174
+ // no module load, no moflo.yaml parse. The feature is only live when the env
2175
+ // override is set or moflo.yaml has an UNcommented `durable_path:` line (the
2176
+ // generated config ships a commented example, which this regex ignores).
2177
+ const durableYamlPath = resolve(projectRoot, 'moflo.yaml');
2178
+ let durableEnabled = Boolean(process.env.MOFLO_DURABLE_PATH);
2179
+ if (!durableEnabled && existsSync(durableYamlPath)) {
2180
+ try {
2181
+ durableEnabled = /^[ \t]*durable_path[ \t]*:/m.test(readFileSync(durableYamlPath, 'utf-8'));
2182
+ } catch { /* unreadable yaml — treat as not-configured */ }
2183
+ }
2184
+ const durablePaths = durableEnabled
2185
+ ? [
2186
+ resolve(projectRoot, 'node_modules/moflo/dist/src/cli/services/durable-sync.js'),
2187
+ resolve(projectRoot, 'dist/src/cli/services/durable-sync.js'),
2188
+ ]
2189
+ : [];
2190
+ const durablePath = durablePaths.find((p) => existsSync(p));
2191
+ if (durablePath) {
2192
+ const { syncDurableAtSessionStart } = await import(pathToFileURL(durablePath).href);
2193
+ const result = await syncDurableAtSessionStart({ projectRoot });
2194
+ if (result?.seededToLocal > 0) {
2195
+ emitMutation(
2196
+ 'seeded shared learnings',
2197
+ `${plural(result.seededToLocal, 'durable entry')} pulled from the shared store`,
2198
+ );
2199
+ }
2200
+ if (result?.flushedToShared > 0) {
2201
+ emitMutation(
2202
+ 'shared local learnings',
2203
+ `${plural(result.flushedToShared, 'durable entry')} pushed to the shared store`,
2204
+ );
2205
+ }
2206
+ }
2207
+ } catch (err) {
2208
+ // Non-fatal — durable sync reconciles on the next session start.
2209
+ try {
2210
+ const msg = err && err.message ? err.message : String(err);
2211
+ process.stderr.write(`durable-memory sync skipped: ${msg}\n`);
2212
+ } catch { /* writing the failure itself must not throw */ }
2213
+ }
2214
+
2109
2215
  // ── 3e-1057. Run unmet schema migrations BEFORE daemon spawn ────────────────
2110
2216
  // run-migrations.mjs walks `bin/migrations/*.mjs` and invokes each that has
2111
2217
  // not been recorded in `.moflo/migrations.json`. Each migration opens sql.js
@@ -2175,6 +2281,58 @@ function runMigrationsAndAnnounce(runnerPath) {
2175
2281
  }
2176
2282
  }
2177
2283
 
2284
+ // ── 3e-1234. Import the git-tracked team learnings artifact BEFORE daemon spawn ─
2285
+ // Opt-in (#1234, epic #1231): when memory.team_artifact (or MOFLO_TEAM_ARTIFACT)
2286
+ // points at a committed JSONL file, merge it into the local durable namespaces
2287
+ // so teammates' learnings (pulled via git) are present when the daemon builds
2288
+ // its HNSW index in section 4. INSERT OR IGNORE → idempotent + first-write-wins.
2289
+ // Fully guarded: a disabled/absent artifact is a silent no-op, and any failure
2290
+ // is non-fatal (the next session-start retries). Runs every session (teammates
2291
+ // pull frequently without a moflo version bump), unlike the upgrade-gated
2292
+ // cherry-pick in section 3.
2293
+ try {
2294
+ // Cheap pre-gate so solo users (the overwhelming majority) pay nothing here —
2295
+ // no module load, no moflo.yaml parse. The feature is only live when an env
2296
+ // override is set, the default artifact already exists, or moflo.yaml has an
2297
+ // UNcommented `team_artifact:` line (the generated config ships a commented
2298
+ // example, which this regex deliberately ignores). Only then import the service.
2299
+ const defaultArtifact = resolve(projectRoot, '.moflo', 'shared', 'learnings.jsonl');
2300
+ const yamlPath = resolve(projectRoot, 'moflo.yaml');
2301
+ let teamEnabled = Boolean(process.env.MOFLO_TEAM_ARTIFACT) || existsSync(defaultArtifact);
2302
+ if (!teamEnabled && existsSync(yamlPath)) {
2303
+ try {
2304
+ teamEnabled = /^[ \t]*team_artifact[ \t]*:/m.test(readFileSync(yamlPath, 'utf-8'));
2305
+ } catch { /* unreadable yaml — treat as not-configured */ }
2306
+ }
2307
+ const teamSyncPaths = teamEnabled
2308
+ ? [
2309
+ resolve(projectRoot, 'node_modules/moflo/dist/src/cli/services/team-artifact-sync.js'),
2310
+ resolve(projectRoot, 'dist/src/cli/services/team-artifact-sync.js'),
2311
+ ]
2312
+ : [];
2313
+ const teamSyncPath = teamSyncPaths.find((p) => existsSync(p));
2314
+ if (teamSyncPath) {
2315
+ const mod = await import(pathToFileURL(teamSyncPath).href);
2316
+ if (typeof mod.resolveTeamArtifactPath === 'function' && typeof mod.importTeamArtifact === 'function') {
2317
+ const artifactPath = mod.resolveTeamArtifactPath(projectRoot);
2318
+ if (artifactPath && existsSync(artifactPath)) {
2319
+ const report = mod.importTeamArtifact({ projectRoot, artifactPath });
2320
+ if (report?.imported > 0) {
2321
+ emitMutation(
2322
+ 'merged team learnings',
2323
+ `${plural(report.imported, 'shared learning')} imported from the git-tracked team artifact`,
2324
+ );
2325
+ }
2326
+ }
2327
+ }
2328
+ }
2329
+ } catch (err) {
2330
+ try {
2331
+ const msg = err && err.message ? err.message : String(err);
2332
+ process.stderr.write(`team-artifact import skipped: ${msg}\n`);
2333
+ } catch { /* stderr write must not throw */ }
2334
+ }
2335
+
2178
2336
  // ── 3f. Flip the upgrade notice to "completed" (#636, #738) ─────────────────
2179
2337
  // See the TTL rationale at the constants above for why we switch to a
2180
2338
  // short-TTL completed badge instead of clearing the file.
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Doctor check: shared full-`moflo.db` detection (#1235, epic #1231).
3
+ *
4
+ * Cross-installation learning-sharing is SAFE when only the durable slice
5
+ * (`learnings`, `knowledge`) travels — via `memory.durable_path` (#1232),
6
+ * `flo memory sync` (#1233), or the git-tracked team artifact (#1234). It is
7
+ * UNSAFE to share the *whole* `moflo.db` between two installs (two git
8
+ * worktrees, two Conductor workspaces, a symlinked `.moflo/`): node:sqlite+WAL
9
+ * keeps concurrent writers from losing rows, but each daemon builds its own
10
+ * in-memory HNSW index, so a write from install A never updates install B's
11
+ * index — searches silently return stale results, and the structural namespaces
12
+ * (`code-map`, guidance chunks) churn across branches.
13
+ *
14
+ * This check warns when the project is *configured toward* a shared full DB and
15
+ * blesses the durable-only setup (no warning). It is pure `fs` (realpath +
16
+ * lstat) — no shelling to `grep`/`ps` (Rule #1).
17
+ *
18
+ * @module cli/commands/doctor-checks-shared-db
19
+ */
20
+ import * as fs from 'fs';
21
+ import * as path from 'path';
22
+ import { memoryDbPath } from '../services/moflo-paths.js';
23
+ import { findProjectRoot } from '../services/project-root.js';
24
+ import { loadMofloConfig } from '../config/moflo-config.js';
25
+ import { errorDetail } from '../shared/utils/error-detail.js';
26
+ const NAME = 'Shared Full DB';
27
+ /**
28
+ * Resolve a path through symlinks for identity comparison. Realpaths BOTH sides
29
+ * of any later `===` (macOS `/var`→`/private/var`, Windows case-fold) per Rule #1;
30
+ * falls back to the absolute literal when the target doesn't exist yet.
31
+ */
32
+ function realOrAbs(p) {
33
+ try {
34
+ return fs.realpathSync(p);
35
+ }
36
+ catch {
37
+ return path.resolve(p);
38
+ }
39
+ }
40
+ /**
41
+ * True when `p` resolves to a full `moflo.db`. Compared case-insensitively —
42
+ * Windows + macOS filesystems are case-insensitive, so a `durable_path` of
43
+ * `MOFLO.DB` is still a full DB (Rule #1). Any path ending in `.../moflo.db`
44
+ * has basename `moflo.db`, so basename alone covers the nested case too.
45
+ */
46
+ function looksLikeFullDb(absPath) {
47
+ return path.basename(absPath).toLowerCase() === 'moflo.db';
48
+ }
49
+ export async function checkSharedFullDb(deps) {
50
+ try {
51
+ const root = deps?.root ?? findProjectRoot();
52
+ const localDb = memoryDbPath(root);
53
+ const config = deps?.config ?? loadMofloConfig(root);
54
+ // `durable_path` ships in #1232 and may not exist in the config type on a
55
+ // branch without it — read defensively so this check stands alone.
56
+ const mem = config.memory;
57
+ const durableRaw = typeof mem.durable_path === 'string' ? mem.durable_path.trim() : '';
58
+ // 1. A symlinked local moflo.db is the classic "two installs, one full DB"
59
+ // setup (a symlinked `.moflo/` across Conductor workspaces / worktrees).
60
+ if (fs.existsSync(localDb)) {
61
+ let isLink = false;
62
+ try {
63
+ isLink = fs.lstatSync(localDb).isSymbolicLink();
64
+ }
65
+ catch {
66
+ /* lstat race / perms — treat as not-a-link */
67
+ }
68
+ if (isLink) {
69
+ return {
70
+ name: NAME,
71
+ status: 'warn',
72
+ message: `${localDb} is a symlink — sharing one full moflo.db across installs makes each daemon's ` +
73
+ `in-memory HNSW index diverge, so search returns stale results (node:sqlite+WAL prevents row ` +
74
+ `loss, not index drift). Keep a local DB and share only the durable slice via memory.durable_path.`,
75
+ fix: 'Replace the symlinked moflo.db with a real local DB, then set memory.durable_path to a dedicated durable-only store.',
76
+ };
77
+ }
78
+ }
79
+ // 2. `durable_path` pointed at a FULL moflo.db (aliases the local DB, or is
80
+ // itself a canonical .moflo/moflo.db) — that shares the whole DB, the very
81
+ // thing durable-only sharing exists to avoid.
82
+ if (durableRaw) {
83
+ const absDurable = path.isAbsolute(durableRaw) ? path.resolve(durableRaw) : path.resolve(root, durableRaw);
84
+ const aliasesLocal = fs.existsSync(absDurable) && fs.existsSync(localDb) && realOrAbs(absDurable) === realOrAbs(localDb);
85
+ if (aliasesLocal || looksLikeFullDb(absDurable)) {
86
+ return {
87
+ name: NAME,
88
+ status: 'warn',
89
+ message: `memory.durable_path (${durableRaw}) points at a full moflo.db — that shares the whole database, ` +
90
+ `causing HNSW index divergence / stale search across installs. Point it at a dedicated durable-only ` +
91
+ `store (a file NOT named moflo.db) so only learnings/knowledge travel.`,
92
+ fix: 'Set memory.durable_path to a dedicated file such as <shared>/moflo-learnings.db (not a full moflo.db).',
93
+ };
94
+ }
95
+ // Blessed: a dedicated durable-only store. Structural namespaces stay local.
96
+ return {
97
+ name: NAME,
98
+ status: 'pass',
99
+ message: `Durable-only shared store configured (${durableRaw}) — safe; structural namespaces stay local.`,
100
+ };
101
+ }
102
+ return {
103
+ name: NAME,
104
+ status: 'pass',
105
+ message: 'No shared full moflo.db detected (local DB is not symlinked; no durable_path override).',
106
+ };
107
+ }
108
+ catch (e) {
109
+ return {
110
+ name: NAME,
111
+ status: 'warn',
112
+ message: `Unable to check shared-DB config: ${errorDetail(e, { firstLineOnly: true })}`,
113
+ };
114
+ }
115
+ }
116
+ //# sourceMappingURL=doctor-checks-shared-db.js.map
@@ -153,7 +153,9 @@ export async function checkWritersAudit(cwd = process.cwd()) {
153
153
  name,
154
154
  status: 'fail',
155
155
  message: `${foreign.length} non-daemon writer(s) running concurrently with daemon (PID ${daemonPid}): ${detail}. ` +
156
- `Each will clobber the daemon's sql.js snapshot on flush #1054 bug class.`,
156
+ `Each bypasses the daemon's single-writer routing (#981): under node:sqlite+WAL they no longer clobber ` +
157
+ `the DB file (the sql.js whole-snapshot flush #1054 guarded against is gone), but concurrent writes leave ` +
158
+ `the daemon's in-memory HNSW index stale, so search returns outdated results until it reindexes.`,
157
159
  fix: process.platform === 'win32'
158
160
  ? `taskkill /F /PID ${foreign.map((p) => p.pid).join(' /PID ')}`
159
161
  : `kill ${foreign.map((p) => p.pid).join(' ')}`,
@@ -9,6 +9,7 @@ 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';
11
11
  import { checkWritersAudit } from './doctor-checks-writers-audit.js';
12
+ import { checkSharedFullDb } from './doctor-checks-shared-db.js';
12
13
  import { checkSwarmFunctional, checkHiveMindFunctional, } from './doctor-checks-swarm.js';
13
14
  import { checkMemoryAccessFunctional } from './doctor-checks-memory-access.js';
14
15
  import { checkBuildTools, checkClaudeCode, checkDiskSpace, checkGit, checkGitRepo, checkNodeVersion, checkNpmVersion, } from './doctor-checks-runtime.js';
@@ -43,6 +44,9 @@ export const allChecks = [
43
44
  checkDaemonOrphan,
44
45
  checkDaemonWriteRouting,
45
46
  checkWritersAudit,
47
+ // #1235 — warn when configured toward a shared *full* moflo.db (symlinked DB
48
+ // or durable_path aliasing a full DB); blesses a durable-only shared store.
49
+ checkSharedFullDb,
46
50
  checkMemoryDatabase,
47
51
  // Surfaces nested `.moflo/moflo.db` directories — every nested instance is
48
52
  // a daemon island in a monorepo (#1174). Runs cheap (depth-bounded BFS,
@@ -118,6 +122,9 @@ export const componentMap = {
118
122
  'orphans': checkDaemonOrphan,
119
123
  'writers-audit': checkWritersAudit,
120
124
  'writers': checkWritersAudit,
125
+ 'shared-db': checkSharedFullDb,
126
+ 'shared-full-db': checkSharedFullDb,
127
+ 'sharing': checkSharedFullDb,
121
128
  'memory': checkMemoryDatabase,
122
129
  'nested-moflo': checkNestedMofloIslands,
123
130
  'nested': checkNestedMofloIslands,