moflo 4.9.0 → 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.
- package/.claude/helpers/gate.cjs +1 -1
- package/bin/gate.cjs +1 -1
- package/bin/lib/moflo-paths.mjs +7 -216
- package/bin/session-start-launcher.mjs +96 -83
- package/dist/src/cli/commands/memory.js +60 -2
- package/dist/src/cli/init/helpers-generator.js +1 -1
- package/dist/src/cli/services/cherry-pick-learnings.js +209 -0
- package/dist/src/cli/services/moflo-paths.js +12 -256
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
package/.claude/helpers/gate.cjs
CHANGED
|
@@ -78,7 +78,7 @@ function loadGateConfig() {
|
|
|
78
78
|
var config = loadGateConfig();
|
|
79
79
|
var command = process.argv[2];
|
|
80
80
|
|
|
81
|
-
var EXEMPT = ['.claude/', '.claude\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state', 'node_modules'];
|
|
81
|
+
var EXEMPT = ['.claude/', '.claude\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state', 'node_modules', 'moflo.yaml'];
|
|
82
82
|
var DANGEROUS = ['rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:', 'mkfs.', '> /dev/sda'];
|
|
83
83
|
var DIRECTIVE_RE = /^(yes|no|yeah|yep|nope|sure|ok|okay|correct|right|exactly|perfect)\b/i;
|
|
84
84
|
var TASK_RE = /\b(fix|bug|error|implement|add|create|build|write|refactor|debug|test|feature|issue|security|optimi)\b/i;
|
package/bin/gate.cjs
CHANGED
|
@@ -78,7 +78,7 @@ function loadGateConfig() {
|
|
|
78
78
|
var config = loadGateConfig();
|
|
79
79
|
var command = process.argv[2];
|
|
80
80
|
|
|
81
|
-
var EXEMPT = ['.claude/', '.claude\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state', 'node_modules'];
|
|
81
|
+
var EXEMPT = ['.claude/', '.claude\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state', 'node_modules', 'moflo.yaml'];
|
|
82
82
|
var DANGEROUS = ['rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:', 'mkfs.', '> /dev/sda'];
|
|
83
83
|
var DIRECTIVE_RE = /^(yes|no|yeah|yep|nope|sure|ok|okay|correct|right|exactly|perfect)\b/i;
|
|
84
84
|
var TASK_RE = /\b(fix|bug|error|implement|add|create|build|write|refactor|debug|test|feature|issue|security|optimi)\b/i;
|
package/bin/lib/moflo-paths.mjs
CHANGED
|
@@ -1,29 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure-JS counterpart to src/cli/services/moflo-paths.ts
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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 {
|
|
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.
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
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
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
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)
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
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
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
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
|
-
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
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
|
}
|
|
@@ -274,7 +274,7 @@ function loadGateConfig() {
|
|
|
274
274
|
var config = loadGateConfig();
|
|
275
275
|
var command = process.argv[2];
|
|
276
276
|
|
|
277
|
-
var EXEMPT = ['.claude/', '.claude\\\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state', 'node_modules'];
|
|
277
|
+
var EXEMPT = ['.claude/', '.claude\\\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state', 'node_modules', 'moflo.yaml'];
|
|
278
278
|
var DANGEROUS = ['rm -rf /', 'format c:', 'del /s /q c:\\\\', ':(){:|:&};:', 'mkfs.', '> /dev/sda'];
|
|
279
279
|
var DIRECTIVE_RE = /^(yes|no|yeah|yep|nope|sure|ok|okay|correct|right|exactly|perfect)\\b/i;
|
|
280
280
|
var TASK_RE = /\\b(fix|bug|error|implement|add|create|build|write|refactor|debug|test|feature|issue|security|optimi)\\b/i;
|
|
@@ -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
|
|
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/`;
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
15
|
-
* `bin/session-start-launcher.mjs` can
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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 {
|
|
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
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.9.
|
|
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.
|
|
82
|
+
"moflo": "^4.9.1",
|
|
83
83
|
"tsx": "^4.21.0",
|
|
84
84
|
"typescript": "^5.9.3",
|
|
85
85
|
"vitest": "^4.0.0"
|