moflo 4.9.1 → 4.9.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.
@@ -1,29 +1,16 @@
1
1
  /**
2
- * Pure-JS counterpart to src/cli/services/moflo-paths.ts (#699, #727).
2
+ * Pure-JS counterpart to src/cli/services/moflo-paths.ts.
3
3
  *
4
4
  * Lives in bin/lib because session-start-launcher.mjs and other bin/ scripts
5
5
  * run before any TS compilation has happened — they can't import the .ts
6
6
  * source. The TS version is the canonical programmatic API; this version
7
- * mirrors the same algorithm so migration also runs from the consumer
8
- * launcher path. Algorithm parity is enforced by the parity case in
9
- * src/cli/__tests__/services/moflo-paths-migration.test.ts.
7
+ * exposes the same path constants + helpers.
8
+ *
9
+ * Per #851, the legacy `.claude-flow/` rename + `.swarm/memory.db` byte-copy
10
+ * helpers no longer ship: the version-bump-gated cherry-pick lives entirely
11
+ * in the launcher and the TS service `cli/services/cherry-pick-learnings.ts`.
10
12
  */
11
- import {
12
- closeSync,
13
- copyFileSync,
14
- existsSync,
15
- mkdirSync,
16
- openSync,
17
- readFileSync,
18
- readSync,
19
- readdirSync,
20
- renameSync,
21
- rmdirSync,
22
- statSync,
23
- unlinkSync,
24
- writeFileSync,
25
- } from 'node:fs';
26
- import { dirname, join } from 'node:path';
13
+ import { join } from 'node:path';
27
14
 
28
15
  export const MOFLO_DIR = '.moflo';
29
16
  export const MEMORY_DB_FILE = 'moflo.db';
@@ -70,199 +57,3 @@ export function memoryDbCandidatePaths(projectRoot) {
70
57
  join(projectRoot, '.claude', LEGACY_MEMORY_DB_FILE),
71
58
  ];
72
59
  }
73
-
74
- /**
75
- * One-time migration of `.claude-flow/` → `.moflo/`. Idempotent — safe to call
76
- * on every session start. See moflo-paths.ts for the full contract.
77
- *
78
- * Returns `{ migrated, reason?, movedCount?, collisions? }`. The launcher
79
- * uses `movedCount` for the "migrated N files" message and `collisions` to
80
- * warn about subdirs (e.g. models/) that exist in both locations.
81
- */
82
- export function migrateClaudeFlowToMoflo(projectRoot) {
83
- const legacy = legacyClaudeFlowDir(projectRoot);
84
- const target = mofloDir(projectRoot);
85
-
86
- if (!existsSync(legacy)) return { migrated: false, reason: 'no-legacy' };
87
-
88
- if (!existsSync(target)) {
89
- let movedCount = 0;
90
- try { movedCount = readdirSync(legacy).length; } catch { /* count is cosmetic */ }
91
- renameSync(legacy, target);
92
- rewriteEmbeddingsModelPath(projectRoot);
93
- return { migrated: true, movedCount };
94
- }
95
-
96
- let entries;
97
- try {
98
- entries = readdirSync(legacy);
99
- } catch {
100
- return { migrated: false, reason: 'legacy-unreadable' };
101
- }
102
-
103
- let moved = 0;
104
- let modelsMoved = false;
105
- const collisions = [];
106
- for (const name of entries) {
107
- const dst = join(target, name);
108
- if (existsSync(dst)) {
109
- collisions.push(name);
110
- continue;
111
- }
112
- try {
113
- renameSync(join(legacy, name), dst);
114
- moved++;
115
- if (name === 'models') modelsMoved = true;
116
- } catch {
117
- // Best-effort — single failed move shouldn't abort the rest.
118
- }
119
- }
120
-
121
- try {
122
- if (readdirSync(legacy).length === 0) rmdirSync(legacy);
123
- } catch {
124
- // Non-fatal — leftover legacy dir means migration runs next time.
125
- }
126
-
127
- if (modelsMoved) rewriteEmbeddingsModelPath(projectRoot);
128
-
129
- if (moved === 0) {
130
- return { migrated: false, reason: 'merged-nothing', movedCount: 0, collisions };
131
- }
132
- return { migrated: true, movedCount: moved, collisions };
133
- }
134
-
135
- /**
136
- * Rewrite `.moflo/embeddings.json:modelPath` if it still references the
137
- * legacy `.claude-flow/` location (#735). Best-effort: file-not-present,
138
- * malformed JSON, missing field, or already-correct path → silent no-op.
139
- * Mirrors the TS twin in src/cli/services/moflo-paths.ts. Uses tmp+rename
140
- * so SIGINT mid-flush can't leave embeddings.json truncated.
141
- */
142
- export function rewriteEmbeddingsModelPath(projectRoot) {
143
- const cfgPath = join(projectRoot, MOFLO_DIR, 'embeddings.json');
144
-
145
- let raw;
146
- try { raw = readFileSync(cfgPath, 'utf8'); } catch { return false; }
147
-
148
- let cfg;
149
- try { cfg = JSON.parse(raw); } catch { return false; }
150
-
151
- if (typeof cfg.modelPath !== 'string') return false;
152
- if (!cfg.modelPath.includes(LEGACY_CLAUDE_FLOW_DIR)) return false;
153
-
154
- cfg.modelPath = cfg.modelPath.split(LEGACY_CLAUDE_FLOW_DIR).join(MOFLO_DIR);
155
-
156
- const tmpPath = `${cfgPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
157
- try {
158
- writeFileSync(tmpPath, JSON.stringify(cfg, null, 2));
159
- renameSync(tmpPath, cfgPath);
160
- return true;
161
- } catch {
162
- try { unlinkSync(tmpPath); } catch { /* best-effort cleanup */ }
163
- return false;
164
- }
165
- }
166
-
167
- const SQLITE_MAGIC_HEADER = Buffer.from('SQLite format 3\0', 'utf8');
168
-
169
- function looksLikeSqliteFile(filePath) {
170
- let fd = null;
171
- try {
172
- fd = openSync(filePath, 'r');
173
- const buf = Buffer.alloc(SQLITE_MAGIC_HEADER.length);
174
- const read = readSync(fd, buf, 0, buf.length, 0);
175
- if (read < SQLITE_MAGIC_HEADER.length) return false;
176
- return buf.equals(SQLITE_MAGIC_HEADER);
177
- } catch {
178
- return false;
179
- } finally {
180
- if (fd !== null) try { closeSync(fd); } catch { /* non-fatal */ }
181
- }
182
- }
183
-
184
- function verifyByteEqual(srcPath, dstPath) {
185
- try {
186
- const srcStat = statSync(srcPath);
187
- const dstStat = statSync(dstPath);
188
- if (srcStat.size !== dstStat.size) return false;
189
- const srcBuf = readFileSync(srcPath);
190
- const dstBuf = readFileSync(dstPath);
191
- return srcBuf.equals(dstBuf);
192
- } catch {
193
- return false;
194
- }
195
- }
196
-
197
- function tryUnlink(filePath) {
198
- try { unlinkSync(filePath); } catch { /* non-fatal */ }
199
- }
200
-
201
- function migrateHnswIndex(projectRoot) {
202
- const src = legacyHnswIndexPath(projectRoot);
203
- const dst = hnswIndexPath(projectRoot);
204
-
205
- if (!existsSync(src)) return false;
206
- if (existsSync(dst)) return false;
207
-
208
- try {
209
- mkdirSync(dirname(dst), { recursive: true });
210
- copyFileSync(src, dst);
211
- } catch {
212
- tryUnlink(dst);
213
- return false;
214
- }
215
-
216
- if (!verifyByteEqual(src, dst)) {
217
- tryUnlink(dst);
218
- return false;
219
- }
220
-
221
- tryUnlink(src);
222
- return true;
223
- }
224
-
225
- /**
226
- * One-time relocation of memory DB from `.swarm/memory.db` → `.moflo/moflo.db`
227
- * (story #727). Idempotent. See moflo-paths.ts for the full contract and the
228
- * SQLite-header / byte-equal verification rationale.
229
- *
230
- * MUST run before any long-lived sql.js consumer (MCP server, daemon) opens
231
- * the DB — sql.js dumps the whole snapshot on every flush and would clobber
232
- * the relocated file. Launcher section 0b is the safe boundary.
233
- */
234
- export function migrateMemoryDbToMoflo(projectRoot) {
235
- const target = memoryDbPath(projectRoot);
236
- if (existsSync(target)) return { migrated: false, reason: 'target-exists' };
237
-
238
- const source = legacyMemoryDbPath(projectRoot);
239
- if (!existsSync(source)) return { migrated: false, reason: 'no-legacy' };
240
-
241
- try {
242
- mkdirSync(dirname(target), { recursive: true });
243
- } catch {
244
- return { migrated: false, reason: 'copy-failed' };
245
- }
246
-
247
- try {
248
- copyFileSync(source, target);
249
- } catch {
250
- tryUnlink(target);
251
- return { migrated: false, reason: 'copy-failed' };
252
- }
253
-
254
- if (!verifyByteEqual(source, target) || !looksLikeSqliteFile(target)) {
255
- tryUnlink(target);
256
- return { migrated: false, reason: 'verify-failed' };
257
- }
258
-
259
- const hnswMoved = migrateHnswIndex(projectRoot);
260
-
261
- try {
262
- renameSync(source, legacyMemoryDbBakPath(projectRoot));
263
- } catch {
264
- return { migrated: true, reason: 'rename-failed', hnswMoved };
265
- }
266
-
267
- return { migrated: true, hnswMoved };
268
- }
@@ -11,7 +11,7 @@ import { spawn, execFileSync } from 'child_process';
11
11
  import { existsSync, readFileSync, writeFileSync, copyFileSync, unlinkSync, readdirSync, mkdirSync, statSync } from 'fs';
12
12
  import { resolve, dirname, join } from 'path';
13
13
  import { fileURLToPath } from 'url';
14
- import { migrateClaudeFlowToMoflo, migrateMemoryDbToMoflo, mofloDir } from './lib/moflo-paths.mjs';
14
+ import { mofloDir } from './lib/moflo-paths.mjs';
15
15
  import { repairMemoryDbIfCorrupt } from './lib/db-repair.mjs';
16
16
 
17
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -116,56 +116,21 @@ try {
116
116
  unlinkSync(join(mofloDir(projectRoot), 'upgrade-notice.json'));
117
117
  } catch { /* non-fatal — file usually doesn't exist */ }
118
118
 
119
- // ── 0. LEGACY state migration (#699, #735) ──────────────────────────────────
120
- // Consumers upgrading from older moflo builds (inherited from upstream Ruflo)
121
- // get a one-time auto-migration of LEGACY `.claude-flow/` → `.moflo/` so claim
122
- // files, models cache, metrics, and the version stamp survive the rename.
123
- // The migration helper is idempotent see bin/lib/moflo-paths.mjs for the
124
- // algorithm.
119
+ // ── 0. Legacy whole-DB / directory migrations have been retired (#851) ─────
120
+ // LEGACY-V2: Pre-#851 the launcher renamed `.claude-flow/` `.moflo/` and
121
+ // byte-copied `.swarm/memory.db` → `.moflo/moflo.db` on every session start.
122
+ // Both blocks ran silently against a daemon that was still holding the old
123
+ // paths in memory, leaving consumers with ghost runtime files reappearing
124
+ // in legacy dirs and a `.gitignore` deletion that exposed 30+ daemon-state
125
+ // files for commit. See docs/moflo-4.9.1-upgrade-experience-2026-05-02.md
126
+ // for the full UX failure report.
125
127
  //
126
- // Staged removal contract (#735):
127
- // 1. THIS release ships Phase 1 (writers redirected to `.moflo/`) + Phase 2 // LEGACY
128
- // (this migration call moves stragglers + warns on collisions).
129
- // 2. The release AFTER Phase 1 is steady-state should hard-delete any
130
- // remaining empty `.claude-flow/` directory — until then, the helper // LEGACY
131
- // drops the dir naturally once everything's been moved.
132
- // LEGACY: every emit below stops firing once `.claude-flow/` is gone.
133
- try {
134
- const cfMigration = migrateClaudeFlowToMoflo(projectRoot);
135
- if (cfMigration?.migrated) {
136
- const count = cfMigration.movedCount ?? 0;
137
- emitMutation(`migrated ${plural(count, 'entry')} from legacy .claude-flow/`); // LEGACY
138
- }
139
- // Surface collisions so users notice that BOTH locations now hold the same
140
- // subdir name (most often `models/` after a partial pre-#735 migration).
141
- // Manual cleanup is needed — moflo refuses to silently choose.
142
- if ((cfMigration?.collisions?.length ?? 0) > 0) {
143
- const collisionMsg = 'kept legacy .claude-flow/ entries to avoid clobbering .moflo/'; // LEGACY
144
- emitMutation(collisionMsg, `collisions: ${cfMigration.collisions.join(', ')}`);
145
- }
146
- } catch {
147
- // Non-fatal — anything left behind by the migration just means it runs
148
- // again next session. Better to keep launching than to block on it.
149
- }
150
-
151
- // ── 0b. LEGACY memory DB relocation (#727) ──────────────────────────────────
152
- // Run BEFORE long-lived sql.js consumers (MCP server, daemon) — see the
153
- // `migrateMemoryDbToMoflo` JSDoc for the copy-verify-delete contract and
154
- // the sql.js write-back hazard.
155
- try {
156
- const dbMigration = migrateMemoryDbToMoflo(projectRoot);
157
- if (dbMigration?.migrated) {
158
- const detail = dbMigration.hnswMoved
159
- ? '.swarm/memory.db → .moflo/moflo.db (with hnsw.index)'
160
- : '.swarm/memory.db → .moflo/moflo.db';
161
- emitMutation('relocated memory db', detail);
162
- if (dbMigration.reason === 'rename-failed') {
163
- emitMutation('legacy .swarm/memory.db remains', 'rename to .bak failed — flo doctor will warn');
164
- }
165
- }
166
- } catch {
167
- // Non-fatal — failed migration leaves both DBs in place; next session retries.
168
- }
128
+ // The version-bump-gated cherry-pick now lives inside section 3 (which is
129
+ // already the gate on the `.moflo/moflo-version` stamp). It stops the daemon
130
+ // first, then `INSERT OR IGNORE`s only the user-authored `learnings` /
131
+ // `knowledge` namespaces every other DB row is derived and rebuilds via
132
+ // the indexers. LEGACY-V2 directories (`.swarm/`, `.claude-flow/`) are left
133
+ // in place as recovery sources; users delete them at their leisure.
169
134
 
170
135
  // ── 0c. Memory DB index repair (#743) ───────────────────────────────────────
171
136
  // The .moflo/moflo.db SQLite file accumulates index corruption ("row N missing
@@ -215,16 +180,14 @@ function fireAndForget(cmd, args, label) {
215
180
  }
216
181
  }
217
182
 
218
- // Stop the daemon recorded in `lockFile` (if any) and start a fresh one. Used
219
- // from two recycle paths in this launcher: (a) the version-bump branch when
220
- // installed moflo just changed, and (b) the stale-daemon branch when the
221
- // running daemon predates the current install by a meaningful margin.
183
+ // Stop the daemon recorded in `lockFile` (if any) without restarting. Used by
184
+ // the upgrade flow before any DB work the daemon must not be holding old
185
+ // path resolution in memory, and a concurrent sql.js flush would clobber the
186
+ // cherry-picked rows. Returns true when a live PID was actually killed.
222
187
  //
223
- // Reads the lock, SIGTERMs the recorded PID, removes the lock, and fires a
224
- // `daemon start --quiet` against `node_modules/moflo/bin/cli.js`. Every
225
- // failure mode (no lock, dead PID, missing CLI) is silently absorbed — the
226
- // recycle is best-effort and must never block session start.
227
- function recycleDaemon(lockFile, label) {
188
+ // Section 4's `hooks.mjs session-start` spawn is responsible for starting a
189
+ // fresh daemon under the current code; this function intentionally does not.
190
+ function stopDaemon(lockFile) {
228
191
  if (!existsSync(lockFile)) return false;
229
192
  let stalePid = null;
230
193
  try {
@@ -235,16 +198,20 @@ function recycleDaemon(lockFile, label) {
235
198
  try { process.kill(stalePid, 'SIGTERM'); } catch { /* already dead */ }
236
199
  }
237
200
  try { unlinkSync(lockFile); } catch { /* non-fatal */ }
238
- // Respawn only if a live daemon was actually recorded — no point starting
239
- // one when there wasn't one before.
240
- if (stalePid !== null) {
241
- const localCliPath = resolve(projectRoot, 'node_modules/moflo/bin/cli.js');
242
- if (existsSync(localCliPath)) {
243
- fireAndForget('node', [localCliPath, 'daemon', 'start', '--quiet'], label);
244
- }
245
- return true;
201
+ return stalePid !== null;
202
+ }
203
+
204
+ // Stop-and-restart helper for the stale-daemon branch (section 3a-pre). The
205
+ // version-bump branch uses stopDaemon directly + relies on section 4 for the
206
+ // fresh start.
207
+ function recycleDaemon(lockFile, label) {
208
+ const stopped = stopDaemon(lockFile);
209
+ if (!stopped) return false;
210
+ const localCliPath = resolve(projectRoot, 'node_modules/moflo/bin/cli.js');
211
+ if (existsSync(localCliPath)) {
212
+ fireAndForget('node', [localCliPath, 'daemon', 'start', '--quiet'], label);
246
213
  }
247
- return false;
214
+ return true;
248
215
  }
249
216
 
250
217
  // ── 2. Reset workflow state for new session ──────────────────────────────────
@@ -326,6 +293,63 @@ try {
326
293
  // migration). See #738 — section 3f flips this to a 2-min "completed"
327
294
  // badge once work finishes (TTL rationale at the constants above).
328
295
  writeUpgradeNotice('in-progress');
296
+
297
+ // Stop the daemon BEFORE any DB writes (#851). It was started under the
298
+ // previous moflo image and holds old path resolution + module cache in
299
+ // memory; a concurrent sql.js flush would clobber the cherry-picked
300
+ // rows below, and old-path writes would resurrect ghost files in legacy
301
+ // dirs. Section 4's `hooks.mjs session-start` spawns a fresh daemon
302
+ // under the current code once 3g writes the version stamp.
303
+ const upgradeDaemonLock = resolve(projectRoot, '.moflo', 'daemon.lock');
304
+ if (stopDaemon(upgradeDaemonLock)) {
305
+ emitMutation('stopped daemon for upgrade', 'will restart fresh after upgrade work');
306
+ }
307
+
308
+ // Cherry-pick durable rows from any legacy DBs (#851). Replaces the
309
+ // pre-#851 full-DB byte-copy migration. The service is idempotent
310
+ // (INSERT OR IGNORE on UNIQUE(namespace, key)) so an aborted launcher
311
+ // re-runs cleanly without duplicate rows. Sources are read-only —
312
+ // .swarm/memory.db is preserved as a recovery source.
313
+ try {
314
+ const cherryPickPaths = [
315
+ resolve(projectRoot, 'node_modules/moflo/dist/src/cli/services/cherry-pick-learnings.js'),
316
+ resolve(projectRoot, 'dist/src/cli/services/cherry-pick-learnings.js'),
317
+ ];
318
+ const cherryPickPath = cherryPickPaths.find((p) => existsSync(p));
319
+ if (cherryPickPath) {
320
+ const mod = await import(`file://${cherryPickPath.replace(/\\/g, '/')}`);
321
+ if (typeof mod.cherryPickLearningsFromLegacy === 'function') {
322
+ const result = await mod.cherryPickLearningsFromLegacy({ projectRoot });
323
+ if (result.copied > 0) {
324
+ emitMutation(
325
+ 'copied learnings forward',
326
+ `${plural(result.copied, 'learning/knowledge entry')} cherry-picked from legacy db`,
327
+ );
328
+ }
329
+ // LEGACY-V2: One-time hint that legacy dirs are recoverable.
330
+ // Only emit when the user actually has legacy state — silent
331
+ // fast-path for fresh installs and consumers who already cleaned
332
+ // up. The legacy dirs are intentionally never auto-deleted; they
333
+ // exist as recovery sources for the cherry-pick (#851).
334
+ const hasLegacy =
335
+ existsSync(resolve(projectRoot, '.swarm', 'memory.db')) || // LEGACY-V2
336
+ existsSync(resolve(projectRoot, '.swarm', 'memory.db.bak')) || // LEGACY-V2
337
+ existsSync(resolve(projectRoot, '.claude-flow')); // LEGACY-V2
338
+ if (hasLegacy) {
339
+ emitMutation(
340
+ 'legacy .swarm/ + .claude-flow/ left in place', // LEGACY-V2
341
+ 'safe to delete — derived data rebuilds on demand',
342
+ );
343
+ }
344
+ }
345
+ }
346
+ } catch (err) {
347
+ try {
348
+ const msg = err && err.message ? err.message : String(err);
349
+ process.stderr.write(`cherry-pick learnings skipped: ${msg}\n`);
350
+ } catch { /* stderr write must not throw */ }
351
+ }
352
+
329
353
  const binDir = resolve(projectRoot, 'node_modules/moflo/bin');
330
354
 
331
355
  // ── Manifest-based auto-update ──────────────────────────────────────
@@ -480,21 +504,10 @@ try {
480
504
  emitMutation('cleaned up retired files', `${removedFiles} removed`);
481
505
  }
482
506
 
483
- // Recycle the running daemon its in-process module cache holds the
484
- // previous moflo image. After an upgrade that cache is stale, which
485
- // shows up as warnings from removed code paths (e.g. the
486
- // `[neural-tools] @moflo/embeddings not resolvable` spam from #639,
487
- // emitted by pre-#592 collapse code that no longer exists in source)
488
- // and means freshly-disabled workers keep running.
489
- //
490
- // `daemon.autoStart` only governs the cold-start case (no daemon
491
- // existed); here a daemon was actually running, so replacing it with a
492
- // current-code copy is the desired behaviour regardless of that flag.
493
- try {
494
- if (recycleDaemon(resolve(projectRoot, '.moflo', 'daemon.lock'), 'daemon-recycle')) {
495
- emitMutation('recycled daemon', 'load fresh moflo code');
496
- }
497
- } catch { /* non-fatal — daemon recycle is best-effort */ }
507
+ // The daemon was already stopped above so the lock file is gone and
508
+ // there's no live PID to recycle here. Section 4's `hooks.mjs
509
+ // session-start` will spawn a fresh daemon under the current moflo
510
+ // image once 3g writes the version stamp.
498
511
 
499
512
  // Manifest reflects synced files immediately; version stamp is deferred
500
513
  // to 3g so an aborted launcher re-runs upgrade detection (#730).
@@ -2582,11 +2582,68 @@ const refreshCommand = {
2582
2582
  return { success: true };
2583
2583
  },
2584
2584
  };
2585
+ // Manual recovery for legacy DBs the launcher's auto cherry-pick can't reach
2586
+ // — schema mismatches that made the auto-run skip, an even older legacy DB
2587
+ // the candidate list doesn't include, or a friend's exported DB.
2588
+ const restoreLearningsCommand = {
2589
+ name: 'restore-learnings',
2590
+ description: 'Cherry-pick learnings/knowledge entries from a legacy DB into .moflo/moflo.db',
2591
+ options: [
2592
+ {
2593
+ name: 'from',
2594
+ description: 'Path to the legacy DB to read from (e.g. .swarm/memory.db)',
2595
+ type: 'string',
2596
+ required: true,
2597
+ },
2598
+ ],
2599
+ examples: [
2600
+ { command: 'flo memory restore-learnings --from .swarm/memory.db', description: 'Recover from a legacy memory DB' },
2601
+ { command: 'flo memory restore-learnings --from .swarm/memory.db.bak', description: 'Recover from a post-upgrade .bak' },
2602
+ ],
2603
+ action: async (ctx) => {
2604
+ const from = ctx.flags.from;
2605
+ if (!from) {
2606
+ output.printError('Source DB path is required. Use --from <path>');
2607
+ return { success: false, exitCode: 1 };
2608
+ }
2609
+ const sourcePath = pathModule.resolve(from);
2610
+ if (!fs.existsSync(sourcePath)) {
2611
+ output.printError(`Source DB not found: ${sourcePath}`);
2612
+ return { success: false, exitCode: 1 };
2613
+ }
2614
+ try {
2615
+ const { cherryPickLearningsFromLegacy, CHERRY_PICK_SKIP_REASONS } = await import('../services/cherry-pick-learnings.js');
2616
+ const result = await cherryPickLearningsFromLegacy({
2617
+ projectRoot: process.cwd(),
2618
+ legacyPaths: [sourcePath],
2619
+ });
2620
+ const report = result.sources[0];
2621
+ if (report?.reason === CHERRY_PICK_SKIP_REASONS.SCHEMA_MISMATCH) {
2622
+ output.printWarning(`Source DB has no memory_entries table — nothing to copy: ${sourcePath}`);
2623
+ return { success: true, data: result };
2624
+ }
2625
+ if (report?.reason === CHERRY_PICK_SKIP_REASONS.OPEN_FAILED) {
2626
+ output.printError(`Could not open source DB: ${sourcePath}`);
2627
+ return { success: false, exitCode: 1, data: result };
2628
+ }
2629
+ output.printSuccess(`Cherry-picked ${result.copied} of ${result.considered} learning/knowledge entries from ${sourcePath}`);
2630
+ if (result.copied < result.considered) {
2631
+ output.printInfo(`${result.considered - result.copied} duplicate row${result.considered - result.copied === 1 ? '' : 's'} skipped (INSERT OR IGNORE)`);
2632
+ }
2633
+ output.printInfo(`Target: ${result.target}`);
2634
+ return { success: true, data: result };
2635
+ }
2636
+ catch (error) {
2637
+ output.printError(`restore-learnings failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
2638
+ return { success: false, exitCode: 1 };
2639
+ }
2640
+ },
2641
+ };
2585
2642
  // Main memory command
2586
2643
  export const memoryCommand = {
2587
2644
  name: 'memory',
2588
2645
  description: 'Memory management commands',
2589
- subcommands: [initMemoryCommand, storeCommand, retrieveCommand, searchCommand, listCommand, deleteCommand, statsCommand, configureCommand, cleanupCommand, compressCommand, exportCommand, importCommand, indexGuidanceCommand, rebuildIndexCommand, codeMapCommand, refreshCommand],
2646
+ subcommands: [initMemoryCommand, storeCommand, retrieveCommand, searchCommand, listCommand, deleteCommand, statsCommand, configureCommand, cleanupCommand, compressCommand, exportCommand, importCommand, indexGuidanceCommand, rebuildIndexCommand, codeMapCommand, refreshCommand, restoreLearningsCommand],
2590
2647
  options: [],
2591
2648
  examples: [
2592
2649
  { command: 'claude-flow memory store -k "key" -v "value"', description: 'Store data' },
@@ -2616,7 +2673,8 @@ export const memoryCommand = {
2616
2673
  `${output.highlight('index-guidance')} - Index .claude/guidance/ files with RAG segments`,
2617
2674
  `${output.highlight('rebuild-index')} - Regenerate embeddings for memory entries`,
2618
2675
  `${output.highlight('code-map')} - Generate structural code map`,
2619
- `${output.highlight('refresh')} - Reindex all content, rebuild embeddings, cleanup, and vacuum`
2676
+ `${output.highlight('refresh')} - Reindex all content, rebuild embeddings, cleanup, and vacuum`,
2677
+ `${output.highlight('restore-learnings')} - Cherry-pick learnings/knowledge from a legacy DB`
2620
2678
  ]);
2621
2679
  return { success: true };
2622
2680
  }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Selective cherry-pick of durable memory rows on upgrade (#851).
3
+ *
4
+ * Replaces the full-DB byte-copy migration that epic #726 / story #727
5
+ * shipped (`migrateMemoryDbToMoflo`). That approach moved the entire 50+ MB
6
+ * of accumulated memory state across paths, then left the daemon writing to
7
+ * stale paths from its in-memory module cache. The user-visible result was
8
+ * "git went haywire" — see docs/moflo-4.9.1-upgrade-experience-2026-05-02.md.
9
+ *
10
+ * Almost all DB content is derived (code-map, embeddings, patterns, guidance
11
+ * chunks) and rebuilds on demand from the indexers. Only the `learnings`
12
+ * and `knowledge` namespaces are user-authored and worth carrying forward
13
+ * across upgrades. Everything else is regenerated cheaply.
14
+ *
15
+ * Algorithm:
16
+ * 1. Probe legacy DB candidates (`.swarm/memory.db.bak`, `.swarm/memory.db`,
17
+ * etc.) read-only — sources are NEVER mutated.
18
+ * 2. Ensure the target `.moflo/moflo.db` exists with V3 schema (idempotent
19
+ * `CREATE TABLE IF NOT EXISTS`).
20
+ * 3. `SELECT … WHERE namespace IN ('learnings', 'knowledge')` from each
21
+ * legacy source.
22
+ * 4. `INSERT OR IGNORE INTO memory_entries` keyed on the existing
23
+ * `UNIQUE(namespace, key)` constraint — duplicates skip silently, which
24
+ * is what makes a re-run of an interrupted migration safe.
25
+ * 5. `atomicWriteFileSync` the target so a SIGINT mid-flush can't truncate.
26
+ *
27
+ * Caller (the launcher) is responsible for stopping the daemon before this
28
+ * runs — sql.js holds a full snapshot in memory and a concurrent flush from
29
+ * a stale daemon would clobber the cherry-picked rows.
30
+ *
31
+ * @module cli/services/cherry-pick-learnings
32
+ */
33
+ /* eslint-disable @typescript-eslint/no-explicit-any */
34
+ import * as fs from 'fs';
35
+ import * as path from 'path';
36
+ import { mofloImport } from './moflo-require.js';
37
+ import { atomicWriteFileSync } from './atomic-file-write.js';
38
+ import { legacyMemoryDbBakPath, memoryDbCandidatePaths, memoryDbPath, } from './moflo-paths.js';
39
+ import { MEMORY_SCHEMA_V3 } from '../memory/memory-initializer.js';
40
+ /** Namespaces preserved across upgrades. Everything else is derived. */
41
+ export const DURABLE_NAMESPACES = ['learnings', 'knowledge'];
42
+ /**
43
+ * Reasons a single source contributed zero rows. Exported so callers can
44
+ * branch on the cause without duplicating string literals.
45
+ */
46
+ export const CHERRY_PICK_SKIP_REASONS = {
47
+ OPEN_FAILED: 'open-failed',
48
+ SCHEMA_MISMATCH: 'schema-mismatch',
49
+ NO_ROWS: 'no-rows',
50
+ SELF_REFERENCE: 'self-reference',
51
+ };
52
+ /**
53
+ * Composed off `memoryDbCandidatePaths` so any future addition there
54
+ * (statusline, doctor, etc. all share that probe order) automatically
55
+ * extends the cherry-pick. The `.bak` path is added at highest priority
56
+ * because it's the post-#727 migration backup and ranks above the live
57
+ * legacy file. The canonical `.moflo/moflo.db` is dropped — the
58
+ * self-reference guard would skip it anyway, and including it would just
59
+ * confuse the report.
60
+ */
61
+ function defaultLegacyCandidates(projectRoot) {
62
+ const canonical = memoryDbPath(projectRoot);
63
+ const tail = memoryDbCandidatePaths(projectRoot).filter((p) => p !== canonical);
64
+ return [legacyMemoryDbBakPath(projectRoot), ...tail];
65
+ }
66
+ /**
67
+ * Cherry-pick durable memory rows from any legacy DBs into `.moflo/moflo.db`.
68
+ *
69
+ * Returns a report — never throws on per-source failures. The caller (launcher)
70
+ * surfaces the count via `emitMutation`; a missing source / schema mismatch /
71
+ * locked file is recorded in `sources[].reason` and the upgrade continues.
72
+ *
73
+ * Hard failures (sql.js unavailable, target write failure) propagate so the
74
+ * launcher can choose to swallow + retry next session-start.
75
+ */
76
+ export async function cherryPickLearningsFromLegacy(options = {}) {
77
+ const projectRoot = options.projectRoot ?? process.cwd();
78
+ const target = path.resolve(options.toPath ?? memoryDbPath(projectRoot));
79
+ const namespaces = options.namespaces ?? DURABLE_NAMESPACES;
80
+ const legacyPaths = (options.legacyPaths ?? defaultLegacyCandidates(projectRoot)).map((p) => path.resolve(p));
81
+ const result = {
82
+ copied: 0,
83
+ considered: 0,
84
+ sources: [],
85
+ target,
86
+ };
87
+ const initSqlJs = (await mofloImport('sql.js'))?.default;
88
+ if (!initSqlJs)
89
+ return result;
90
+ const SQL = (await initSqlJs());
91
+ fs.mkdirSync(path.dirname(target), { recursive: true });
92
+ const targetExists = fs.existsSync(target);
93
+ const targetDb = targetExists
94
+ ? new SQL.Database(fs.readFileSync(target))
95
+ : new SQL.Database();
96
+ let insertStmt = null;
97
+ try {
98
+ targetDb.run(MEMORY_SCHEMA_V3);
99
+ const placeholders = namespaces.map(() => '?').join(',');
100
+ const selectSql = `SELECT id, key, namespace, content, type, embedding, embedding_model, ` +
101
+ `embedding_dimensions, tags, metadata, owner_id, created_at, updated_at, status ` +
102
+ `FROM memory_entries WHERE namespace IN (${placeholders})`;
103
+ // Hoisted prepare — avoids re-parsing the SQL inside sql.js for every
104
+ // INSERT. Matters for legacy DBs with hundreds of learnings rows.
105
+ insertStmt = targetDb.prepare(`INSERT OR IGNORE INTO memory_entries ` +
106
+ `(id, key, namespace, content, type, embedding, embedding_model, ` +
107
+ ` embedding_dimensions, tags, metadata, owner_id, created_at, updated_at, status) ` +
108
+ `VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
109
+ for (const sourcePath of legacyPaths) {
110
+ if (sourcePath === target) {
111
+ result.sources.push({
112
+ path: sourcePath,
113
+ rowsRead: 0,
114
+ rowsInserted: 0,
115
+ reason: CHERRY_PICK_SKIP_REASONS.SELF_REFERENCE,
116
+ });
117
+ continue;
118
+ }
119
+ if (!fs.existsSync(sourcePath))
120
+ continue;
121
+ const report = readAndInsert(SQL, sourcePath, targetDb, insertStmt, selectSql, namespaces);
122
+ result.sources.push(report);
123
+ result.copied += report.rowsInserted;
124
+ result.considered += report.rowsRead;
125
+ }
126
+ // Skip the atomic write when there's nothing to persist:
127
+ // - copied=0 + target didn't exist → don't materialize an empty DB
128
+ // (the regular initializer creates it on first real write).
129
+ // - copied=0 + target already existed → no diff, nothing to flush.
130
+ if (result.copied > 0) {
131
+ atomicWriteFileSync(target, Buffer.from(targetDb.export()));
132
+ }
133
+ }
134
+ finally {
135
+ if (insertStmt) {
136
+ try {
137
+ insertStmt.free();
138
+ }
139
+ catch { /* best-effort cleanup */ }
140
+ }
141
+ targetDb.close();
142
+ }
143
+ return result;
144
+ }
145
+ function readAndInsert(SQL, sourcePath, targetDb, insertStmt, selectSql, namespaces) {
146
+ let sourceDb;
147
+ try {
148
+ sourceDb = new SQL.Database(fs.readFileSync(sourcePath));
149
+ }
150
+ catch {
151
+ return {
152
+ path: sourcePath,
153
+ rowsRead: 0,
154
+ rowsInserted: 0,
155
+ reason: CHERRY_PICK_SKIP_REASONS.OPEN_FAILED,
156
+ };
157
+ }
158
+ try {
159
+ // Older / unrelated DBs may not have memory_entries.
160
+ const probe = sourceDb.exec(`SELECT name FROM sqlite_master WHERE type='table' AND name='memory_entries' LIMIT 1`);
161
+ if (!probe[0]?.values?.[0]) {
162
+ return {
163
+ path: sourcePath,
164
+ rowsRead: 0,
165
+ rowsInserted: 0,
166
+ reason: CHERRY_PICK_SKIP_REASONS.SCHEMA_MISMATCH,
167
+ };
168
+ }
169
+ let rowsRead = 0;
170
+ let rowsInserted = 0;
171
+ const selectStmt = sourceDb.prepare(selectSql);
172
+ selectStmt.bind(namespaces.slice());
173
+ while (selectStmt.step()) {
174
+ rowsRead++;
175
+ const row = selectStmt.getAsObject();
176
+ insertStmt.bind([
177
+ row.id,
178
+ row.key,
179
+ row.namespace,
180
+ row.content,
181
+ row.type ?? 'semantic',
182
+ row.embedding ?? null,
183
+ row.embedding_model ?? null,
184
+ row.embedding_dimensions ?? null,
185
+ row.tags ?? null,
186
+ row.metadata ?? null,
187
+ row.owner_id ?? null,
188
+ row.created_at ?? null,
189
+ row.updated_at ?? null,
190
+ row.status ?? 'active',
191
+ ]);
192
+ insertStmt.step();
193
+ if (targetDb.getRowsModified() > 0)
194
+ rowsInserted++;
195
+ insertStmt.reset();
196
+ }
197
+ selectStmt.free();
198
+ return {
199
+ path: sourcePath,
200
+ rowsRead,
201
+ rowsInserted,
202
+ reason: rowsRead === 0 ? CHERRY_PICK_SKIP_REASONS.NO_ROWS : undefined,
203
+ };
204
+ }
205
+ finally {
206
+ sourceDb.close();
207
+ }
208
+ }
209
+ //# sourceMappingURL=cherry-pick-learnings.js.map
@@ -1,24 +1,25 @@
1
1
  /**
2
- * MoFlo runtime state directory constants + legacy migrations (#699, #727).
2
+ * MoFlo runtime state directory constants.
3
3
  *
4
4
  * MoFlo owns its state under `.moflo/` at the project root. The upstream Ruflo
5
- * fork used `.claude-flow/`; consumers upgrading from older moflo builds (which
6
- * inherited that path) get a one-time auto-migration so they don't lose claim
7
- * files, daemon state, metrics, etc.
5
+ * fork used `.claude-flow/`; both legacy locations are still recognized as
6
+ * read-only sources for the version-bump-gated cherry-pick (#851) but are
7
+ * never relocated or renamed automatically — leaving them in place gives
8
+ * consumers a recovery source and avoids the failure modes that motivated
9
+ * the issue (silent migrations, daemon-held stale paths, .gitignore deletion).
8
10
  *
9
11
  * Anything that touches a runtime state path under the project root must
10
12
  * compose it from `MOFLO_DIR`. Plain string literals like `'.moflo'` are
11
13
  * tolerated where a constant import is awkward (shell templates, tests) but
12
14
  * production code is checked by `published-package-drift-guard.test.ts`.
13
15
  *
14
- * The pure-JS twin at `bin/lib/moflo-paths.mjs` mirrors the algorithm so
15
- * `bin/session-start-launcher.mjs` can run the migration before any TS has
16
- * been compiled. The parity test in moflo-paths-migration.test.ts catches
17
- * algorithm divergence between the two.
16
+ * The pure-JS twin at `bin/lib/moflo-paths.mjs` mirrors these constants so
17
+ * `bin/session-start-launcher.mjs` can resolve paths before any TS is
18
+ * compiled. The cherry-pick logic itself lives in
19
+ * `cli/services/cherry-pick-learnings.ts` and is dynamically imported from
20
+ * the compiled `dist/` by the launcher.
18
21
  */
19
- import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmdirSync, statSync, unlinkSync, } from 'node:fs';
20
- import { dirname, join } from 'node:path';
21
- import { atomicWriteFileSync } from '../shared/utils/atomic-file-write.js';
22
+ import { join } from 'node:path';
22
23
  export const MOFLO_DIR = '.moflo';
23
24
  /** Canonical memory DB filename (post-#727). Lives at `<root>/.moflo/moflo.db`. */
24
25
  export const MEMORY_DB_FILE = 'moflo.db';
@@ -77,249 +78,4 @@ export function memoryDbCandidatePaths(projectRoot) {
77
78
  join(projectRoot, '.claude', LEGACY_MEMORY_DB_FILE),
78
79
  ];
79
80
  }
80
- /**
81
- * One-time migration of `.claude-flow/` → `.moflo/`.
82
- *
83
- * - Legacy missing → no-op (the steady state after first run).
84
- * - Legacy present + target missing → atomic rename (preserves mtimes).
85
- * - Both present → merge: target wins on collision, leaving the colliding
86
- * entry behind in legacy/ so the launcher can warn and a future run can
87
- * retry. Drops the legacy dir if everything moved cleanly.
88
- *
89
- * When the models cache moves (legacy `.claude-flow/models/` lands at
90
- * `.moflo/models/`), `.moflo/embeddings.json:modelPath` is rewritten in the
91
- * same call so the next embedder run finds the relocated ONNX bytes instead
92
- * of re-downloading multi-MB binaries (#735).
93
- *
94
- * Idempotent and safe to call from session start.
95
- */
96
- export function migrateClaudeFlowToMoflo(projectRoot) {
97
- const legacy = legacyClaudeFlowDir(projectRoot);
98
- const target = mofloDir(projectRoot);
99
- if (!existsSync(legacy))
100
- return { migrated: false, reason: 'no-legacy' };
101
- if (!existsSync(target)) {
102
- // Wholesale rename — count what's about to move so the launcher can
103
- // report `migrated N files` instead of opaque "migrated runtime state".
104
- let movedCount = 0;
105
- try {
106
- movedCount = readdirSync(legacy).length;
107
- }
108
- catch { /* count is cosmetic */ }
109
- renameSync(legacy, target);
110
- rewriteEmbeddingsModelPath(projectRoot);
111
- return { migrated: true, movedCount };
112
- }
113
- let entries;
114
- try {
115
- entries = readdirSync(legacy);
116
- }
117
- catch {
118
- return { migrated: false, reason: 'legacy-unreadable' };
119
- }
120
- let moved = 0;
121
- let modelsMoved = false;
122
- const collisions = [];
123
- for (const name of entries) {
124
- const dst = join(target, name);
125
- if (existsSync(dst)) {
126
- collisions.push(name); // target wins — but the launcher must warn
127
- continue;
128
- }
129
- try {
130
- renameSync(join(legacy, name), dst);
131
- moved++;
132
- if (name === 'models')
133
- modelsMoved = true;
134
- }
135
- catch {
136
- // Best-effort merge — a failed move on one entry shouldn't abort the rest.
137
- }
138
- }
139
- // Drop empty legacy dir so future runs short-circuit at existsSync(legacy).
140
- try {
141
- if (readdirSync(legacy).length === 0)
142
- rmdirSync(legacy);
143
- }
144
- catch {
145
- // Non-fatal — leftover legacy dir just means migration runs next time.
146
- }
147
- if (modelsMoved)
148
- rewriteEmbeddingsModelPath(projectRoot);
149
- if (moved === 0) {
150
- return { migrated: false, reason: 'merged-nothing', movedCount: 0, collisions };
151
- }
152
- return { migrated: true, movedCount: moved, collisions };
153
- }
154
- /**
155
- * Rewrite `.moflo/embeddings.json:modelPath` if it still references the
156
- * legacy `.claude-flow/` location. Called after a models/ relocation so the
157
- * stale path doesn't force a multi-MB re-download (#735).
158
- *
159
- * Best-effort: file-not-present, malformed JSON, missing `modelPath`, or
160
- * already-correct path are all silent no-ops. Returns true only when the
161
- * file was actually rewritten. Uses atomicWriteFileSync so a SIGINT mid-flush
162
- * can't truncate embeddings.json and break every subsequent embedder run.
163
- */
164
- export function rewriteEmbeddingsModelPath(projectRoot) {
165
- const cfgPath = join(projectRoot, MOFLO_DIR, 'embeddings.json');
166
- let raw;
167
- try {
168
- raw = readFileSync(cfgPath, 'utf8');
169
- }
170
- catch {
171
- return false;
172
- }
173
- let cfg;
174
- try {
175
- cfg = JSON.parse(raw);
176
- }
177
- catch {
178
- return false;
179
- }
180
- if (typeof cfg.modelPath !== 'string')
181
- return false;
182
- if (!cfg.modelPath.includes(LEGACY_CLAUDE_FLOW_DIR))
183
- return false;
184
- // split-join handles every occurrence and works with both `/` and `\\`
185
- // (escaped) forms in JSON-encoded Windows paths.
186
- cfg.modelPath = cfg.modelPath.split(LEGACY_CLAUDE_FLOW_DIR).join(MOFLO_DIR);
187
- try {
188
- atomicWriteFileSync(cfgPath, JSON.stringify(cfg, null, 2));
189
- return true;
190
- }
191
- catch {
192
- return false;
193
- }
194
- }
195
- const SQLITE_MAGIC_HEADER = Buffer.from('SQLite format 3\0', 'utf8');
196
- function looksLikeSqliteFile(filePath) {
197
- let fd = null;
198
- try {
199
- fd = openSync(filePath, 'r');
200
- const buf = Buffer.alloc(SQLITE_MAGIC_HEADER.length);
201
- const read = readSync(fd, buf, 0, buf.length, 0);
202
- if (read < SQLITE_MAGIC_HEADER.length)
203
- return false;
204
- return buf.equals(SQLITE_MAGIC_HEADER);
205
- }
206
- catch {
207
- return false;
208
- }
209
- finally {
210
- if (fd !== null)
211
- try {
212
- closeSync(fd);
213
- }
214
- catch { /* non-fatal */ }
215
- }
216
- }
217
- function verifyByteEqual(srcPath, dstPath) {
218
- try {
219
- const srcStat = statSync(srcPath);
220
- const dstStat = statSync(dstPath);
221
- if (srcStat.size !== dstStat.size)
222
- return false;
223
- const srcBuf = readFileSync(srcPath);
224
- const dstBuf = readFileSync(dstPath);
225
- return srcBuf.equals(dstBuf);
226
- }
227
- catch {
228
- return false;
229
- }
230
- }
231
- function tryUnlink(filePath) {
232
- try {
233
- unlinkSync(filePath);
234
- }
235
- catch { /* non-fatal */ }
236
- }
237
- /**
238
- * Move `.swarm/hnsw.index` → `.moflo/hnsw.index` using the same
239
- * copy-verify-delete pattern as the DB. Returns true on success, false when
240
- * source absent or copy/verify failed (caller treats as best-effort).
241
- */
242
- function migrateHnswIndex(projectRoot) {
243
- const src = legacyHnswIndexPath(projectRoot);
244
- const dst = hnswIndexPath(projectRoot);
245
- if (!existsSync(src))
246
- return false;
247
- if (existsSync(dst))
248
- return false; // already there — leave both alone
249
- try {
250
- mkdirSync(dirname(dst), { recursive: true });
251
- copyFileSync(src, dst);
252
- }
253
- catch {
254
- tryUnlink(dst);
255
- return false;
256
- }
257
- if (!verifyByteEqual(src, dst)) {
258
- tryUnlink(dst);
259
- return false;
260
- }
261
- tryUnlink(src); // sidecar can be regenerated; no .bak retention needed
262
- return true;
263
- }
264
- /**
265
- * One-time relocation of the memory DB from the upstream `.swarm/memory.db`
266
- * layout to the canonical `.moflo/moflo.db` (story #727).
267
- *
268
- * Algorithm — copy-verify-delete, never `mv`:
269
- * 1. If `.moflo/moflo.db` exists → no-op (`target-exists`).
270
- * 2. If `.swarm/memory.db` absent → no-op (`no-legacy`).
271
- * 3. Ensure `.moflo/` exists.
272
- * 4. `copyFileSync(.swarm/memory.db, .moflo/moflo.db)`.
273
- * 5. Verify byte-equal (size + content) AND SQLite header magic.
274
- * 6. Move `.swarm/hnsw.index` → `.moflo/hnsw.index` (best-effort).
275
- * 7. Rename `.swarm/memory.db` → `.swarm/memory.db.bak` (kept one upgrade cycle).
276
- *
277
- * Any failure between 4–7 deletes the partial target and returns failure;
278
- * next session-start retries.
279
- *
280
- * MUST run BEFORE any long-lived consumer (MCP server, daemon) opens the DB —
281
- * sql.js holds a full snapshot in RAM and would clobber the relocated file
282
- * on its next flush. The launcher's section before the fire-and-forget block
283
- * is the safe boundary; see feedback memory `feedback_sqljs_writeback_clobber`.
284
- *
285
- * Idempotent.
286
- */
287
- export function migrateMemoryDbToMoflo(projectRoot) {
288
- const target = memoryDbPath(projectRoot);
289
- if (existsSync(target))
290
- return { migrated: false, reason: 'target-exists' };
291
- const source = legacyMemoryDbPath(projectRoot);
292
- if (!existsSync(source))
293
- return { migrated: false, reason: 'no-legacy' };
294
- try {
295
- mkdirSync(dirname(target), { recursive: true });
296
- }
297
- catch {
298
- return { migrated: false, reason: 'copy-failed' };
299
- }
300
- try {
301
- copyFileSync(source, target);
302
- }
303
- catch {
304
- tryUnlink(target);
305
- return { migrated: false, reason: 'copy-failed' };
306
- }
307
- if (!verifyByteEqual(source, target) || !looksLikeSqliteFile(target)) {
308
- tryUnlink(target);
309
- return { migrated: false, reason: 'verify-failed' };
310
- }
311
- const hnswMoved = migrateHnswIndex(projectRoot);
312
- // Final step: retire the source by renaming to .bak. Only after the new
313
- // file is verified — never lose data. If this rename fails we leave the
314
- // source in place; a stale `.swarm/memory.db` next to a healthy
315
- // `.moflo/moflo.db` is harmless (the bridge reads only the new path) and
316
- // surfaces as a `flo doctor` warning.
317
- try {
318
- renameSync(source, legacyMemoryDbBakPath(projectRoot));
319
- }
320
- catch {
321
- return { migrated: true, reason: 'rename-failed', hnswMoved };
322
- }
323
- return { migrated: true, hnswMoved };
324
- }
325
81
  //# sourceMappingURL=moflo-paths.js.map
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.9.1';
5
+ export const VERSION = '4.9.2';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.9.1",
3
+ "version": "4.9.2",
4
4
  "description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
5
5
  "main": "dist/src/cli/index.js",
6
6
  "type": "module",
@@ -79,7 +79,7 @@
79
79
  "@typescript-eslint/eslint-plugin": "^7.18.0",
80
80
  "@typescript-eslint/parser": "^7.18.0",
81
81
  "eslint": "^8.0.0",
82
- "moflo": "^4.9.0",
82
+ "moflo": "^4.9.1",
83
83
  "tsx": "^4.21.0",
84
84
  "typescript": "^5.9.3",
85
85
  "vitest": "^4.0.0"