moflo 4.10.27 → 4.10.29-rc.1

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:
@@ -229,9 +229,15 @@ Confirm the published version matches what we just built.
229
229
 
230
230
  ```bash
231
231
  npm install moflo@<new-version> --save-dev
232
+ npm install --package-lock-only --force
233
+ npm ci --dry-run --legacy-peer-deps
232
234
  ```
233
235
 
234
- This updates `package.json` and `package-lock.json` to use the newly published version as a devDependency.
236
+ The first command updates `package.json` and `package-lock.json` to use the newly published version as a devDependency.
237
+
238
+ The second command (`--package-lock-only --force`) backfills cross-platform optional-dependency entries that npm omits from the lockfile when `npm install` runs on a single host platform. Without it, transitive optional deps like the nested `@rolldown/binding-wasm32-wasi/node_modules/@emnapi/*` entries get dropped when publishing from Windows, and the resulting lockfile fails `npm ci` on Ubuntu/macOS CI legs. This produced the green-`release-smoke` → red-`ci.yml` divergence in run `26487580745` (moflo@4.10.27 install commit).
239
+
240
+ The final `npm ci --dry-run --legacy-peer-deps` proves the resulting lockfile is in sync with `package.json` before we commit it. If this exits non-zero, stop and investigate — do not commit a broken lockfile.
235
241
 
236
242
  ### Step 13: Final Commit
237
243
 
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.
@@ -2037,6 +2037,52 @@ try {
2037
2037
  } catch { /* writing the failure itself must not throw */ }
2038
2038
  }
2039
2039
 
2040
+ // ── 3e-1244. Hydrate a fresh workspace from a whole-DB snapshot BEFORE daemon ─
2041
+ // When `memory.hydrate_from` (or MOFLO_HYDRATE_FROM) points at a snapshot AND
2042
+ // the local .moflo/moflo.db is absent/empty, restore the whole DB (structural +
2043
+ // durable + embeddings) so the first session is searchable without a cold full
2044
+ // reindex (#1244, epic #1231). Runs BEFORE the daemon spawns so the restored
2045
+ // rows are present when the daemon builds its HNSW index, and BEFORE the purge/
2046
+ // sync blocks below so they operate on the hydrated DB. No-clobber: a no-op once
2047
+ // the local DB has content. Best-effort: a failure must never block session start.
2048
+ try {
2049
+ // Cheap pre-gate so the overwhelming majority (no hydrate configured) pay
2050
+ // nothing — no module load, no moflo.yaml parse. Live only when the env
2051
+ // override is set or moflo.yaml has an UNcommented `hydrate_from:` line (the
2052
+ // generated config ships a commented example, which this regex ignores).
2053
+ const hydrateYamlPath = resolve(projectRoot, 'moflo.yaml');
2054
+ let hydrateEnabled = Boolean(process.env.MOFLO_HYDRATE_FROM);
2055
+ if (!hydrateEnabled && existsSync(hydrateYamlPath)) {
2056
+ try {
2057
+ hydrateEnabled = /^[ \t]*hydrate_from[ \t]*:/m.test(readFileSync(hydrateYamlPath, 'utf-8'));
2058
+ } catch { /* unreadable yaml — treat as not-configured */ }
2059
+ }
2060
+ const hydratePaths = hydrateEnabled
2061
+ ? [
2062
+ resolve(projectRoot, 'node_modules/moflo/dist/src/cli/services/snapshot-restore.js'),
2063
+ resolve(projectRoot, 'dist/src/cli/services/snapshot-restore.js'),
2064
+ ]
2065
+ : [];
2066
+ const hydratePath = hydratePaths.find((p) => existsSync(p));
2067
+ if (hydratePath) {
2068
+ const { hydrateAtSessionStart } = await import(pathToFileURL(hydratePath).href);
2069
+ const result = await hydrateAtSessionStart({ projectRoot });
2070
+ if (result?.restored) {
2071
+ const mb = ((result.bytes ?? 0) / (1024 * 1024)).toFixed(1);
2072
+ emitMutation(
2073
+ 'hydrated workspace from snapshot',
2074
+ `restored ${mb} MB whole-DB snapshot — search ready without a cold reindex`,
2075
+ );
2076
+ }
2077
+ }
2078
+ } catch (err) {
2079
+ // Non-fatal — without a snapshot the workspace just cold-indexes as before.
2080
+ try {
2081
+ const msg = err && err.message ? err.message : String(err);
2082
+ process.stderr.write(`snapshot hydration skipped: ${msg}\n`);
2083
+ } catch { /* writing the failure itself must not throw */ }
2084
+ }
2085
+
2040
2086
  // ── 3e-728. Hard-delete leftover soft-delete tombstones (#728) ─────────────
2041
2087
  // Soft-delete was retired in story #728 — `status='deleted'` rows are now
2042
2088
  // unrecoverable bloat from prior moflo versions. Purge any stragglers and
@@ -2106,6 +2152,56 @@ try {
2106
2152
  } catch { /* writing the failure itself must not throw */ }
2107
2153
  }
2108
2154
 
2155
+ // ── 3e-1232. Sync durable namespaces with the shared store BEFORE daemon ────
2156
+ // When `memory.durable_path` (or MOFLO_DURABLE_PATH) is set, flush this
2157
+ // worktree's learnings to the shared store and seed any sibling-workspace
2158
+ // learnings back into the local DB. Runs BEFORE the daemon spawns so the
2159
+ // freshly-seeded rows are present when the daemon builds its in-memory HNSW
2160
+ // index — that's what makes the cross-worktree repro (#1231) pass. No-op when
2161
+ // the feature is off. Best-effort: a sync failure must never block session start.
2162
+ try {
2163
+ // Cheap pre-gate so solo users (the overwhelming majority) pay nothing here —
2164
+ // no module load, no moflo.yaml parse. The feature is only live when the env
2165
+ // override is set or moflo.yaml has an UNcommented `durable_path:` line (the
2166
+ // generated config ships a commented example, which this regex ignores).
2167
+ const durableYamlPath = resolve(projectRoot, 'moflo.yaml');
2168
+ let durableEnabled = Boolean(process.env.MOFLO_DURABLE_PATH);
2169
+ if (!durableEnabled && existsSync(durableYamlPath)) {
2170
+ try {
2171
+ durableEnabled = /^[ \t]*durable_path[ \t]*:/m.test(readFileSync(durableYamlPath, 'utf-8'));
2172
+ } catch { /* unreadable yaml — treat as not-configured */ }
2173
+ }
2174
+ const durablePaths = durableEnabled
2175
+ ? [
2176
+ resolve(projectRoot, 'node_modules/moflo/dist/src/cli/services/durable-sync.js'),
2177
+ resolve(projectRoot, 'dist/src/cli/services/durable-sync.js'),
2178
+ ]
2179
+ : [];
2180
+ const durablePath = durablePaths.find((p) => existsSync(p));
2181
+ if (durablePath) {
2182
+ const { syncDurableAtSessionStart } = await import(pathToFileURL(durablePath).href);
2183
+ const result = await syncDurableAtSessionStart({ projectRoot });
2184
+ if (result?.seededToLocal > 0) {
2185
+ emitMutation(
2186
+ 'seeded shared learnings',
2187
+ `${plural(result.seededToLocal, 'durable entry')} pulled from the shared store`,
2188
+ );
2189
+ }
2190
+ if (result?.flushedToShared > 0) {
2191
+ emitMutation(
2192
+ 'shared local learnings',
2193
+ `${plural(result.flushedToShared, 'durable entry')} pushed to the shared store`,
2194
+ );
2195
+ }
2196
+ }
2197
+ } catch (err) {
2198
+ // Non-fatal — durable sync reconciles on the next session start.
2199
+ try {
2200
+ const msg = err && err.message ? err.message : String(err);
2201
+ process.stderr.write(`durable-memory sync skipped: ${msg}\n`);
2202
+ } catch { /* writing the failure itself must not throw */ }
2203
+ }
2204
+
2109
2205
  // ── 3e-1057. Run unmet schema migrations BEFORE daemon spawn ────────────────
2110
2206
  // run-migrations.mjs walks `bin/migrations/*.mjs` and invokes each that has
2111
2207
  // not been recorded in `.moflo/migrations.json`. Each migration opens sql.js
@@ -2175,6 +2271,58 @@ function runMigrationsAndAnnounce(runnerPath) {
2175
2271
  }
2176
2272
  }
2177
2273
 
2274
+ // ── 3e-1234. Import the git-tracked team learnings artifact BEFORE daemon spawn ─
2275
+ // Opt-in (#1234, epic #1231): when memory.team_artifact (or MOFLO_TEAM_ARTIFACT)
2276
+ // points at a committed JSONL file, merge it into the local durable namespaces
2277
+ // so teammates' learnings (pulled via git) are present when the daemon builds
2278
+ // its HNSW index in section 4. INSERT OR IGNORE → idempotent + first-write-wins.
2279
+ // Fully guarded: a disabled/absent artifact is a silent no-op, and any failure
2280
+ // is non-fatal (the next session-start retries). Runs every session (teammates
2281
+ // pull frequently without a moflo version bump), unlike the upgrade-gated
2282
+ // cherry-pick in section 3.
2283
+ try {
2284
+ // Cheap pre-gate so solo users (the overwhelming majority) pay nothing here —
2285
+ // no module load, no moflo.yaml parse. The feature is only live when an env
2286
+ // override is set, the default artifact already exists, or moflo.yaml has an
2287
+ // UNcommented `team_artifact:` line (the generated config ships a commented
2288
+ // example, which this regex deliberately ignores). Only then import the service.
2289
+ const defaultArtifact = resolve(projectRoot, '.moflo', 'shared', 'learnings.jsonl');
2290
+ const yamlPath = resolve(projectRoot, 'moflo.yaml');
2291
+ let teamEnabled = Boolean(process.env.MOFLO_TEAM_ARTIFACT) || existsSync(defaultArtifact);
2292
+ if (!teamEnabled && existsSync(yamlPath)) {
2293
+ try {
2294
+ teamEnabled = /^[ \t]*team_artifact[ \t]*:/m.test(readFileSync(yamlPath, 'utf-8'));
2295
+ } catch { /* unreadable yaml — treat as not-configured */ }
2296
+ }
2297
+ const teamSyncPaths = teamEnabled
2298
+ ? [
2299
+ resolve(projectRoot, 'node_modules/moflo/dist/src/cli/services/team-artifact-sync.js'),
2300
+ resolve(projectRoot, 'dist/src/cli/services/team-artifact-sync.js'),
2301
+ ]
2302
+ : [];
2303
+ const teamSyncPath = teamSyncPaths.find((p) => existsSync(p));
2304
+ if (teamSyncPath) {
2305
+ const mod = await import(pathToFileURL(teamSyncPath).href);
2306
+ if (typeof mod.resolveTeamArtifactPath === 'function' && typeof mod.importTeamArtifact === 'function') {
2307
+ const artifactPath = mod.resolveTeamArtifactPath(projectRoot);
2308
+ if (artifactPath && existsSync(artifactPath)) {
2309
+ const report = mod.importTeamArtifact({ projectRoot, artifactPath });
2310
+ if (report?.imported > 0) {
2311
+ emitMutation(
2312
+ 'merged team learnings',
2313
+ `${plural(report.imported, 'shared learning')} imported from the git-tracked team artifact`,
2314
+ );
2315
+ }
2316
+ }
2317
+ }
2318
+ }
2319
+ } catch (err) {
2320
+ try {
2321
+ const msg = err && err.message ? err.message : String(err);
2322
+ process.stderr.write(`team-artifact import skipped: ${msg}\n`);
2323
+ } catch { /* stderr write must not throw */ }
2324
+ }
2325
+
2178
2326
  // ── 3f. Flip the upgrade notice to "completed" (#636, #738) ─────────────────
2179
2327
  // See the TTL rationale at the constants above for why we switch to a
2180
2328
  // short-TTL completed badge instead of clearing the file.
@@ -673,6 +673,58 @@ export async function checkMofloYamlCompliance(cwd = process.cwd()) {
673
673
  fix: 'Restart Claude Code (yaml-upgrader auto-appends) or `npx moflo init --force`',
674
674
  };
675
675
  }
676
+ /**
677
+ * #1229 — standing tripwire for a silently-disabled memory_first gate.
678
+ *
679
+ * `gates.memory_first` is the only enforcement of the memory-search-first
680
+ * protocol. When it is `false` in moflo.yaml the protocol is off repo-wide
681
+ * with no per-prompt signal — and because the disabled gate is itself what
682
+ * surfaces the stored "never disable memory_first" learning, the situation
683
+ * self-conceals. Historically a daemon-spawned headless analysis worker could
684
+ * write this value unprompted to unblock itself (the `optimize` worker editing
685
+ * `moflo.yaml` to `memory_first: false # Temporarily disabled for performance
686
+ * analysis`); that root cause is fixed by making those workers read-only, but
687
+ * this check is the loud, standing detector so the state never goes unnoticed
688
+ * again — whatever writes it (worker, agent, or a deliberate consumer edit).
689
+ *
690
+ * Warn rather than fail: disabling the gate is a legitimate (if rare) choice;
691
+ * the point is that it can never be silent. Matches only an *active* (un-
692
+ * commented) `memory_first: false` line and is EOL-agnostic so it behaves
693
+ * identically on CRLF (Windows) and LF (POSIX) checkouts.
694
+ *
695
+ * Exported with a cwd param so tests can target a temp root without touching
696
+ * process.cwd().
697
+ */
698
+ export async function checkMemoryFirstGate(cwd = process.cwd()) {
699
+ const yamlPath = join(cwd, 'moflo.yaml');
700
+ // Absence is covered by checkMofloYamlCompliance; with no file the gate
701
+ // defaults to enabled, so there is nothing to warn about here.
702
+ if (!existsSync(yamlPath)) {
703
+ return { name: 'Memory-First Gate', status: 'pass', message: 'Enabled (default — no moflo.yaml override)' };
704
+ }
705
+ let content;
706
+ try {
707
+ content = readFileSync(yamlPath, 'utf-8');
708
+ }
709
+ catch (e) {
710
+ return { name: 'Memory-First Gate', status: 'warn', message: `Unable to read moflo.yaml: ${errorDetail(e)}` };
711
+ }
712
+ // Active value only: a line whose first non-whitespace token is
713
+ // `memory_first: false`. A commented `# memory_first: false` is ignored
714
+ // because the leading `#` defeats the `^\s*memory_first` anchor.
715
+ const disabled = content
716
+ .split(/\r?\n/)
717
+ .some((line) => /^\s*memory_first:\s*false\b/i.test(line));
718
+ if (disabled) {
719
+ return {
720
+ name: 'Memory-First Gate',
721
+ status: 'warn',
722
+ message: 'DISABLED in moflo.yaml — memory-search-first protocol is off repo-wide and silent per-prompt',
723
+ fix: 'Restore `gates.memory_first: true` (e.g. `git checkout -- moflo.yaml`) unless you disabled it deliberately',
724
+ };
725
+ }
726
+ return { name: 'Memory-First Gate', status: 'pass', message: 'Enabled' };
727
+ }
676
728
  /**
677
729
  * #981 / #987 — surfaces the single-writer-architecture safety net.
678
730
  *
@@ -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,10 +9,11 @@ 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';
15
- import { checkConfigFile, checkDaemonIdentity, checkDaemonOrphan, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMofloYamlCompliance, checkNestedMofloIslands, checkStatusLine, checkSwarmResidue, checkTestDirs, } from './doctor-checks-config.js';
16
+ import { checkConfigFile, checkDaemonIdentity, checkDaemonOrphan, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMemoryFirstGate, checkMofloYamlCompliance, checkNestedMofloIslands, checkStatusLine, checkSwarmResidue, checkTestDirs, } from './doctor-checks-config.js';
16
17
  import { checkSpellEngine, checkSandboxTier } from './doctor-checks-platform.js';
17
18
  import { checkEmbeddings, checkSemanticQuality, } from './doctor-checks-memory.js';
18
19
  import { checkIntelligence } from './doctor-checks-intelligence.js';
@@ -34,6 +35,8 @@ export const allChecks = [
34
35
  checkGitRepo,
35
36
  checkConfigFile,
36
37
  checkMofloYamlCompliance,
38
+ // #1229 — loud tripwire for a silently-disabled memory_first gate.
39
+ checkMemoryFirstGate,
37
40
  checkStatusLine,
38
41
  checkDaemonStatus,
39
42
  checkDaemonVersionSkew,
@@ -41,6 +44,9 @@ export const allChecks = [
41
44
  checkDaemonOrphan,
42
45
  checkDaemonWriteRouting,
43
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,
44
50
  checkMemoryDatabase,
45
51
  // Surfaces nested `.moflo/moflo.db` directories — every nested instance is
46
52
  // a daemon island in a monorepo (#1174). Runs cheap (depth-bounded BFS,
@@ -97,6 +103,8 @@ export const componentMap = {
97
103
  'config': checkConfigFile,
98
104
  'yaml': checkMofloYamlCompliance,
99
105
  'moflo-yaml': checkMofloYamlCompliance,
106
+ 'memory-first': checkMemoryFirstGate,
107
+ 'memory-gate': checkMemoryFirstGate,
100
108
  'statusline': checkStatusLine,
101
109
  'status-line': checkStatusLine,
102
110
  'daemon': checkDaemonStatus,
@@ -114,6 +122,9 @@ export const componentMap = {
114
122
  'orphans': checkDaemonOrphan,
115
123
  'writers-audit': checkWritersAudit,
116
124
  'writers': checkWritersAudit,
125
+ 'shared-db': checkSharedFullDb,
126
+ 'shared-full-db': checkSharedFullDb,
127
+ 'sharing': checkSharedFullDb,
117
128
  'memory': checkMemoryDatabase,
118
129
  'nested-moflo': checkNestedMofloIslands,
119
130
  'nested': checkNestedMofloIslands,