moflo 4.12.11 → 4.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/guidance/shipped/moflo-cli-reference.md +45 -1
- package/.claude/guidance/shipped/moflo-cross-install-memory-sharing.md +7 -2
- package/.claude/guidance/shipped/moflo-skills-reference.md +2 -0
- package/.claude/skills/fl/phases.md +51 -17
- package/.claude/skills/optimize-learnings/SKILL.md +220 -0
- package/README.md +95 -1
- package/bin/lib/get-backend.mjs +150 -12
- package/bin/lib/skill-categories.mjs +1 -0
- package/bin/session-start-launcher.mjs +13 -5
- package/dist/src/cli/commands/daemon.js +5 -2
- package/dist/src/cli/commands/epic.js +5 -1
- package/dist/src/cli/commands/hive-mind.js +6 -4
- package/dist/src/cli/commands/hooks.js +8 -8
- package/dist/src/cli/commands/index.js +5 -0
- package/dist/src/cli/commands/memory-audit-learnings.js +587 -0
- package/dist/src/cli/commands/memory.js +71 -10
- package/dist/src/cli/commands/spell-schedule.js +5 -3
- package/dist/src/cli/commands/worktree.js +408 -0
- package/dist/src/cli/config/moflo-config.js +57 -0
- package/dist/src/cli/index.js +4 -2
- package/dist/src/cli/init/executor.js +1 -0
- package/dist/src/cli/mcp-tools/memory-admin-tools.js +46 -8
- package/dist/src/cli/mcp-tools/moflodb-tools.js +30 -6
- package/dist/src/cli/memory/bridge-entries.js +157 -9
- package/dist/src/cli/memory/controllers/batch-operations.js +7 -2
- package/dist/src/cli/memory/daemon-backend.js +152 -11
- package/dist/src/cli/memory/entries-read.js +47 -2
- package/dist/src/cli/memory/entries-write.js +73 -10
- package/dist/src/cli/memory/hnsw-singleton.js +112 -9
- package/dist/src/cli/memory/learnings-audit.js +420 -0
- package/dist/src/cli/memory/learnings-dead-paths.js +202 -0
- package/dist/src/cli/memory/learnings-tree.js +187 -0
- package/dist/src/cli/memory/memory-bridge.js +37 -27
- package/dist/src/cli/memory/tool-call-markup.js +218 -0
- package/dist/src/cli/parser.js +7 -3
- package/dist/src/cli/services/cherry-pick-learnings.js +9 -3
- package/dist/src/cli/services/durable-reconcile.js +161 -0
- package/dist/src/cli/services/durable-store-io.js +291 -0
- package/dist/src/cli/services/durable-sync.js +159 -24
- package/dist/src/cli/services/team-artifact-sync.js +462 -163
- package/dist/src/cli/services/worktree-provision.js +400 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -19,10 +19,21 @@
|
|
|
19
19
|
* every durable write, so a new learning propagates to sibling workspaces
|
|
20
20
|
* by their next session-start.
|
|
21
21
|
*
|
|
22
|
-
* Both directions are a single call to {@link
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* Both directions are a single call to {@link reconcileDurableStores} with
|
|
23
|
+
* source/target swapped — the same {@link planReconcile} rule the team artifact
|
|
24
|
+
* uses, so a correction and a deletion cross between worktrees instead of being
|
|
25
|
+
* dropped.
|
|
26
|
+
*
|
|
27
|
+
* Until #1463 both directions ran `INSERT OR IGNORE`, making the result the
|
|
28
|
+
* union of durable rows on both sides: an entry purged in one worktree came
|
|
29
|
+
* straight back at the next session-start seed. That mattered more here than
|
|
30
|
+
* for the team artifact, because worktree sharing needs no configuration —
|
|
31
|
+
* `worktree_sharing` defaults on — so every user with a linked worktree had it.
|
|
32
|
+
*
|
|
33
|
+
* `cherryPickLearningsFromLegacy` is deliberately NOT used any more for these
|
|
34
|
+
* two directions, and deliberately still used for the legacy upgrade path it
|
|
35
|
+
* was written for: reconciling against a legacy DB could archive rows on the
|
|
36
|
+
* strength of a store that predates tombstones entirely.
|
|
26
37
|
*
|
|
27
38
|
* Safety: the shared store is a plain transfer DB — moflo never *searches* it
|
|
28
39
|
* directly, so it has no HNSW sidecar and no index-divergence concern. Row
|
|
@@ -39,7 +50,10 @@ import { findProjectRoot } from './project-root.js';
|
|
|
39
50
|
import { stableAbsolute, pickConfiguredPath } from './configured-path.js';
|
|
40
51
|
import { memoryDbPath } from './moflo-paths.js';
|
|
41
52
|
import { loadMofloConfig } from '../config/moflo-config.js';
|
|
42
|
-
import {
|
|
53
|
+
import { openDaemonDatabase } from '../memory/daemon-backend.js';
|
|
54
|
+
import { CHERRY_PICK_SKIP_REASONS, DURABLE_NAMESPACES, isDurableNamespace, } from './cherry-pick-learnings.js';
|
|
55
|
+
import { planReconcile, TOMBSTONE_TTL_MS } from './durable-reconcile.js';
|
|
56
|
+
import { readDurableSnapshot, applyDurableActions, pruneExpiredArchives } from './durable-store-io.js';
|
|
43
57
|
export { isDurableNamespace };
|
|
44
58
|
/**
|
|
45
59
|
* Read `<projectRoot>/.git` ONCE and classify the checkout for worktree sharing,
|
|
@@ -164,18 +178,111 @@ export function resolveDurablePath(projectRoot = findProjectRoot(), config) {
|
|
|
164
178
|
}
|
|
165
179
|
return { path: null, skipped: 'not-configured' };
|
|
166
180
|
}
|
|
181
|
+
const emptyDirection = (target) => ({
|
|
182
|
+
copied: 0,
|
|
183
|
+
considered: 0,
|
|
184
|
+
sources: [],
|
|
185
|
+
target,
|
|
186
|
+
updated: 0,
|
|
187
|
+
archived: 0,
|
|
188
|
+
resurrected: 0,
|
|
189
|
+
keptTarget: 0,
|
|
190
|
+
});
|
|
191
|
+
/**
|
|
192
|
+
* Reconcile the durable slice of `sourcePath` into `targetPath`. Both stores
|
|
193
|
+
* are real memory DBs; the merge rule is the shared one, so a key present only
|
|
194
|
+
* in the target is never touched and only an explicit archive deletes.
|
|
195
|
+
*
|
|
196
|
+
* Missing source → a zero result rather than a throw: "the sibling worktree has
|
|
197
|
+
* not written anything yet" is a normal state, not an error.
|
|
198
|
+
*/
|
|
199
|
+
export function reconcileDurableStores(sourcePath, targetPath, opts = {}) {
|
|
200
|
+
const result = emptyDirection(targetPath);
|
|
201
|
+
if (stableAbsolute(sourcePath) === stableAbsolute(targetPath)) {
|
|
202
|
+
result.sources.push({ path: sourcePath, rowsRead: 0, rowsInserted: 0, reason: CHERRY_PICK_SKIP_REASONS.SELF_REFERENCE });
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
if (!fs.existsSync(sourcePath)) {
|
|
206
|
+
result.sources.push({ path: sourcePath, rowsRead: 0, rowsInserted: 0, reason: CHERRY_PICK_SKIP_REASONS.NO_ROWS });
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
let sourceDb;
|
|
210
|
+
try {
|
|
211
|
+
sourceDb = openDaemonDatabase(sourcePath);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
result.sources.push({ path: sourcePath, rowsRead: 0, rowsInserted: 0, reason: CHERRY_PICK_SKIP_REASONS.OPEN_FAILED });
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
let snapshot;
|
|
218
|
+
try {
|
|
219
|
+
snapshot = readDurableSnapshot(sourceDb, DURABLE_NAMESPACES, { withPayloads: true, key: opts.key });
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
sourceDb.close();
|
|
223
|
+
}
|
|
224
|
+
result.considered = snapshot.records.size;
|
|
225
|
+
if (snapshot.records.size === 0) {
|
|
226
|
+
result.sources.push({ path: sourcePath, rowsRead: 0, rowsInserted: 0, reason: CHERRY_PICK_SKIP_REASONS.NO_ROWS });
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
230
|
+
const targetDb = openDaemonDatabase(targetPath);
|
|
231
|
+
try {
|
|
232
|
+
const { records: target } = readDurableSnapshot(targetDb, DURABLE_NAMESPACES, { key: opts.key });
|
|
233
|
+
const { actions, summary } = planReconcile(snapshot.records, target);
|
|
234
|
+
const applied = applyDurableActions(targetDb, actions, snapshot.payloads);
|
|
235
|
+
result.copied = applied.inserted;
|
|
236
|
+
result.updated = applied.updated;
|
|
237
|
+
result.archived = applied.archived;
|
|
238
|
+
result.resurrected = applied.resurrected;
|
|
239
|
+
result.keptTarget = summary.keptTargetNewer;
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
targetDb.close();
|
|
243
|
+
}
|
|
244
|
+
result.sources.push({
|
|
245
|
+
path: sourcePath,
|
|
246
|
+
rowsRead: result.considered,
|
|
247
|
+
rowsInserted: result.copied,
|
|
248
|
+
});
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Apply the archive retention window to one store. Best-effort: a missing or
|
|
253
|
+
* unopenable store is "nothing to prune", never an error that fails a sync.
|
|
254
|
+
*/
|
|
255
|
+
function pruneStore(dbPath) {
|
|
256
|
+
if (!fs.existsSync(dbPath))
|
|
257
|
+
return 0;
|
|
258
|
+
let db;
|
|
259
|
+
try {
|
|
260
|
+
db = openDaemonDatabase(dbPath);
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
return pruneExpiredArchives(db, Date.now(), TOMBSTONE_TTL_MS);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
db.close();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** Total rows a direction actually changed — what the launcher reports. */
|
|
276
|
+
export function changedRows(result) {
|
|
277
|
+
return result.copied + result.updated + result.archived + result.resurrected;
|
|
278
|
+
}
|
|
167
279
|
/**
|
|
168
280
|
* Seed durable rows from the shared store into this project's local DB.
|
|
169
281
|
* Shared is the source, local `.moflo/moflo.db` the target. No-op when the
|
|
170
282
|
* shared store doesn't exist yet (nothing to seed).
|
|
171
283
|
*/
|
|
172
284
|
export async function seedDurableFromShared(projectRoot, durablePath) {
|
|
173
|
-
return
|
|
174
|
-
projectRoot,
|
|
175
|
-
legacyPaths: [durablePath],
|
|
176
|
-
toPath: memoryDbPath(projectRoot),
|
|
177
|
-
namespaces: DURABLE_NAMESPACES,
|
|
178
|
-
});
|
|
285
|
+
return reconcileDurableStores(durablePath, memoryDbPath(projectRoot));
|
|
179
286
|
}
|
|
180
287
|
/**
|
|
181
288
|
* Flush durable rows from this project's local DB into the shared store.
|
|
@@ -190,19 +297,23 @@ export async function flushDurableToShared(projectRoot, durablePath) {
|
|
|
190
297
|
catch {
|
|
191
298
|
/* a real open failure surfaces from cherry-pick below */
|
|
192
299
|
}
|
|
193
|
-
return
|
|
194
|
-
projectRoot,
|
|
195
|
-
legacyPaths: [memoryDbPath(projectRoot)],
|
|
196
|
-
toPath: durablePath,
|
|
197
|
-
namespaces: DURABLE_NAMESPACES,
|
|
198
|
-
});
|
|
300
|
+
return reconcileDurableStores(memoryDbPath(projectRoot), durablePath);
|
|
199
301
|
}
|
|
200
302
|
/**
|
|
201
303
|
* Bidirectional durable sync run once at session-start: flush local → shared
|
|
202
304
|
* (bootstraps pre-existing local learnings into the shared store), then seed
|
|
203
|
-
* shared → local (pulls in sibling workspaces' learnings). Both directions
|
|
204
|
-
*
|
|
205
|
-
*
|
|
305
|
+
* shared → local (pulls in sibling workspaces' learnings). Both directions run
|
|
306
|
+
* the shared reconciliation rule, so corrections and deletions converge across
|
|
307
|
+
* worktrees instead of accumulating as the union of both sides.
|
|
308
|
+
*
|
|
309
|
+
* Flush-then-seed order is load-bearing for deletions, and so is doing the
|
|
310
|
+
* archive pruning only once BOTH have run — see the prune comment in the body.
|
|
311
|
+
*
|
|
312
|
+
* Safe to call unconditionally, but no longer a true no-op when unconfigured:
|
|
313
|
+
* durable deletes archive rather than drop rows, so the local store still needs
|
|
314
|
+
* its retention window applied with sharing off. That costs one DB open and one
|
|
315
|
+
* indexed probe per session start, and nothing further unless expired archives
|
|
316
|
+
* actually exist.
|
|
206
317
|
*
|
|
207
318
|
* Intended to run BEFORE the daemon starts so the freshly-seeded rows are
|
|
208
319
|
* present when the daemon builds its in-memory HNSW index.
|
|
@@ -211,17 +322,36 @@ export async function syncDurableAtSessionStart(opts = {}) {
|
|
|
211
322
|
const projectRoot = opts.projectRoot ?? findProjectRoot();
|
|
212
323
|
const { path: durablePath, skipped, autoWorktree } = resolveDurablePath(projectRoot, opts.config);
|
|
213
324
|
if (!durablePath) {
|
|
214
|
-
|
|
325
|
+
// Sharing off, but deletes still archive — so the local store still needs
|
|
326
|
+
// its retention window applied. This is the majority case (one checkout,
|
|
327
|
+
// no team artifact); skipping it here is what would let archived rows grow
|
|
328
|
+
// without bound for most users.
|
|
329
|
+
return {
|
|
330
|
+
durablePath: null,
|
|
331
|
+
skipped,
|
|
332
|
+
flushedToShared: 0,
|
|
333
|
+
seededToLocal: 0,
|
|
334
|
+
prunedArchives: pruneStore(memoryDbPath(projectRoot)),
|
|
335
|
+
};
|
|
215
336
|
}
|
|
216
337
|
// Flush first so this worktree's existing learnings land in the shared store
|
|
217
338
|
// before we seed — keeps the very first opt-in symmetric across workspaces.
|
|
218
339
|
const flush = await flushDurableToShared(projectRoot, durablePath);
|
|
219
340
|
const seed = await seedDurableFromShared(projectRoot, durablePath);
|
|
341
|
+
// Prune AFTER both directions, never inside one. Pruning during the flush
|
|
342
|
+
// deletes tombstones the seed has not applied yet: a worktree dormant past
|
|
343
|
+
// the retention window flushes its stale live row first, the shared
|
|
344
|
+
// tombstone correctly wins but is then pruned as expired, and the next
|
|
345
|
+
// flush re-inserts the purged entry into every workspace. Running the seed
|
|
346
|
+
// first means this store has applied the deletion before anything drops the
|
|
347
|
+
// evidence for it.
|
|
348
|
+
const pruned = pruneStore(memoryDbPath(projectRoot)) + pruneStore(durablePath);
|
|
220
349
|
return {
|
|
221
350
|
durablePath,
|
|
222
351
|
autoWorktree: autoWorktree ?? false,
|
|
223
|
-
flushedToShared: flush
|
|
224
|
-
seededToLocal: seed
|
|
352
|
+
flushedToShared: changedRows(flush),
|
|
353
|
+
seededToLocal: changedRows(seed),
|
|
354
|
+
prunedArchives: pruned,
|
|
225
355
|
};
|
|
226
356
|
}
|
|
227
357
|
/**
|
|
@@ -246,7 +376,12 @@ export async function writeThroughDurable(namespace, opts = {}) {
|
|
|
246
376
|
const { path: durablePath } = resolveDurablePath(projectRoot, opts.config);
|
|
247
377
|
if (!durablePath)
|
|
248
378
|
return;
|
|
249
|
-
|
|
379
|
+
// Flush ONLY the key that just changed. A full reconciliation here would
|
|
380
|
+
// read both stores end to end after every single learning written — the
|
|
381
|
+
// caller already knows exactly what moved, so a one-row plan is enough.
|
|
382
|
+
// The session-start sync remains the full-store reconciliation.
|
|
383
|
+
fs.mkdirSync(path.dirname(durablePath), { recursive: true });
|
|
384
|
+
reconcileDurableStores(memoryDbPath(projectRoot), durablePath, { key: opts.key });
|
|
250
385
|
}
|
|
251
386
|
catch {
|
|
252
387
|
// Swallow entirely — see the rationale above.
|