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
package/bin/lib/get-backend.mjs
CHANGED
|
@@ -163,8 +163,10 @@ function openNodeSqlite(dbPath, opts) {
|
|
|
163
163
|
// background indexer holds a write lock for 5–8s during its first
|
|
164
164
|
// full-tree pass after `npm install`. See daemon-backend.ts twin for
|
|
165
165
|
// the full rationale (#1098).
|
|
166
|
-
db.exec(
|
|
167
|
-
db.exec
|
|
166
|
+
db.exec(`PRAGMA busy_timeout = ${OPEN_BUSY_TIMEOUT_MS}`);
|
|
167
|
+
// Not `db.exec` directly: SQLite skips the busy handler for a
|
|
168
|
+
// journal-mode change, so this one pragma needs its own retry (#1471).
|
|
169
|
+
setWalWithRetry(db, dbPath);
|
|
168
170
|
db.exec('PRAGMA synchronous = NORMAL');
|
|
169
171
|
// Phase 4 / #1083 — network-FS detection. SQLite's POSIX advisory locks
|
|
170
172
|
// and WAL shared-memory both fail silently on NFS/SMB; the engine falls
|
|
@@ -179,6 +181,148 @@ function openNodeSqlite(dbPath, opts) {
|
|
|
179
181
|
return wrapNodeSqlite(db, dbPath);
|
|
180
182
|
}
|
|
181
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Shared parking buffer for the journal-mode retry sleep. `Atomics.wait` is
|
|
186
|
+
* the only synchronous sleep that behaves identically on Linux, macOS and
|
|
187
|
+
* Windows without shelling out (Rule #1), and this open path is synchronous.
|
|
188
|
+
*/
|
|
189
|
+
const WAL_SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
|
|
190
|
+
|
|
191
|
+
/** @param {number} ms */
|
|
192
|
+
function sleepMs(ms) {
|
|
193
|
+
Atomics.wait(WAL_SLEEP_BUF, 0, 0, ms);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The open-path `busy_timeout`. Named because two places depend on it being
|
|
198
|
+
* the same number: the pragma below sets it, and `readJournalModeBounded`
|
|
199
|
+
* restores it after narrowing it for a probe.
|
|
200
|
+
*/
|
|
201
|
+
const OPEN_BUSY_TIMEOUT_MS = 15_000;
|
|
202
|
+
/**
|
|
203
|
+
* Budget for the post-exhaustion probe. The query form of `PRAGMA
|
|
204
|
+
* journal_mode` takes a SHARED lock and IS covered by the busy handler, so it
|
|
205
|
+
* would otherwise inherit the full `OPEN_BUSY_TIMEOUT_MS` — doubling the
|
|
206
|
+
* worst case to ~30s before we report anything on the one path where we have
|
|
207
|
+
* already decided to give up.
|
|
208
|
+
*/
|
|
209
|
+
const WAL_PROBE_BUSY_TIMEOUT_MS = 500;
|
|
210
|
+
const WAL_PROBE_ATTEMPTS = 3;
|
|
211
|
+
/** See the daemon-backend.ts twin for the budget rationale (#1471). */
|
|
212
|
+
const WAL_RETRY_BUDGET_MS = OPEN_BUSY_TIMEOUT_MS;
|
|
213
|
+
const WAL_RETRY_MIN_DELAY_MS = 5;
|
|
214
|
+
const WAL_RETRY_MAX_DELAY_MS = 250;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* SQLITE_BUSY (5) and SQLITE_LOCKED (6). The message test is a fallback for
|
|
218
|
+
* wrappers that don't propagate `errcode`.
|
|
219
|
+
*
|
|
220
|
+
* @param {unknown} err
|
|
221
|
+
* @returns {boolean}
|
|
222
|
+
*/
|
|
223
|
+
function isBusyError(err) {
|
|
224
|
+
const e = /** @type {{ errcode?: number, message?: string } | null} */ (err);
|
|
225
|
+
if (e?.errcode === 5 || e?.errcode === 6) return true;
|
|
226
|
+
return /database( table)? is locked/i.test(String(e?.message ?? ''));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Current journal mode, lowercased. `''` when the probe itself fails.
|
|
231
|
+
*
|
|
232
|
+
* @param {object} db
|
|
233
|
+
* @returns {string}
|
|
234
|
+
*/
|
|
235
|
+
function readJournalMode(db) {
|
|
236
|
+
try {
|
|
237
|
+
const row = db.prepare('PRAGMA journal_mode').get();
|
|
238
|
+
return String(row?.journal_mode ?? '').toLowerCase();
|
|
239
|
+
} catch {
|
|
240
|
+
return '';
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* `readJournalMode` under a deliberately narrow busy budget, restoring the
|
|
246
|
+
* open-path budget afterwards so a caller that survives keeps the connection
|
|
247
|
+
* it asked for. Only ever called once the retry budget is already spent.
|
|
248
|
+
*
|
|
249
|
+
* @param {object} db
|
|
250
|
+
* @returns {string}
|
|
251
|
+
*/
|
|
252
|
+
function readJournalModeBounded(db) {
|
|
253
|
+
try {
|
|
254
|
+
try {
|
|
255
|
+
db.exec(`PRAGMA busy_timeout = ${WAL_PROBE_BUSY_TIMEOUT_MS}`);
|
|
256
|
+
} catch {
|
|
257
|
+
// Non-fatal: we still probe, just without the narrower budget.
|
|
258
|
+
}
|
|
259
|
+
for (let attempt = 0; attempt < WAL_PROBE_ATTEMPTS; attempt++) {
|
|
260
|
+
const mode = readJournalMode(db);
|
|
261
|
+
if (mode) return mode;
|
|
262
|
+
}
|
|
263
|
+
return '';
|
|
264
|
+
} finally {
|
|
265
|
+
try {
|
|
266
|
+
db.exec(`PRAGMA busy_timeout = ${OPEN_BUSY_TIMEOUT_MS}`);
|
|
267
|
+
} catch {
|
|
268
|
+
// Non-fatal: the handle is still usable, and every path out of here
|
|
269
|
+
// either throws or hands back a database that is already in WAL.
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Run `PRAGMA journal_mode = WAL`, retrying on contention (#1471).
|
|
276
|
+
*
|
|
277
|
+
* `busy_timeout` is set first and covers every other statement, but SQLite
|
|
278
|
+
* does NOT invoke the busy handler for a journal-mode change — so the one
|
|
279
|
+
* pragma the budget was put there for never gets it, and concurrent
|
|
280
|
+
* first-opens of a fresh database threw `SQLITE_BUSY` immediately, killing
|
|
281
|
+
* whichever process lost the race. On a database already in WAL the pragma is
|
|
282
|
+
* a no-op taking no exclusive lock, so the common path never enters the loop.
|
|
283
|
+
*
|
|
284
|
+
* Twin: `src/cli/memory/daemon-backend.ts:setWalWithRetry`. Keep in lockstep.
|
|
285
|
+
*
|
|
286
|
+
* @param {object} db node:sqlite DatabaseSync handle (or a test fake)
|
|
287
|
+
* @param {string} dbPath
|
|
288
|
+
* @param {number} [budgetMs]
|
|
289
|
+
*/
|
|
290
|
+
export function setWalWithRetry(db, dbPath, budgetMs = WAL_RETRY_BUDGET_MS) {
|
|
291
|
+
let lastErr = null;
|
|
292
|
+
let waited = 0;
|
|
293
|
+
let delay = WAL_RETRY_MIN_DELAY_MS;
|
|
294
|
+
|
|
295
|
+
for (;;) {
|
|
296
|
+
try {
|
|
297
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
298
|
+
return;
|
|
299
|
+
} catch (err) {
|
|
300
|
+
lastErr = err;
|
|
301
|
+
// Anything that isn't contention — a corrupt file, a read-only mount —
|
|
302
|
+
// will not clear by waiting. Surface it now rather than after 15s.
|
|
303
|
+
if (!isBusyError(err)) throw err;
|
|
304
|
+
}
|
|
305
|
+
if (waited >= budgetMs) break;
|
|
306
|
+
const nap = Math.min(delay, budgetMs - waited);
|
|
307
|
+
sleepMs(nap);
|
|
308
|
+
waited += nap;
|
|
309
|
+
delay = Math.min(delay * 2, WAL_RETRY_MAX_DELAY_MS);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Budget spent. Another opener may have completed the conversion while we
|
|
313
|
+
// were losing races — the database being in WAL is the outcome we wanted,
|
|
314
|
+
// whichever process got it there.
|
|
315
|
+
const mode = readJournalModeBounded(db);
|
|
316
|
+
if (mode === 'wal') return;
|
|
317
|
+
|
|
318
|
+
throw new Error(
|
|
319
|
+
`[moflo] PRAGMA journal_mode = WAL stayed busy for ${waited}ms on ${dbPath} ` +
|
|
320
|
+
`(journal_mode is still "${mode || 'unreadable'}"). Another process is holding an ` +
|
|
321
|
+
`exclusive lock on the database. Original error: ${String(lastErr?.message ?? lastErr)}`,
|
|
322
|
+
{ cause: lastErr },
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
182
326
|
/**
|
|
183
327
|
* Read `journal_mode` back after we requested WAL. If the engine returned a
|
|
184
328
|
* different mode (`delete`, `truncate`, `persist`, `memory`, `off`), the
|
|
@@ -196,16 +340,10 @@ function openNodeSqlite(dbPath, opts) {
|
|
|
196
340
|
*/
|
|
197
341
|
export function warnIfNotWal(db, dbPath) {
|
|
198
342
|
if (_networkFsWarnedPaths.has(dbPath)) return;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
mode = String(row?.journal_mode ?? '').toLowerCase();
|
|
204
|
-
} catch {
|
|
205
|
-
// Probe must never break the open path — silent failure is acceptable
|
|
206
|
-
// because the WAL pragma above already either took effect or didn't.
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
343
|
+
// A probe that throws yields '' and falls through the guard below without
|
|
344
|
+
// warning — the WAL pragma above either took effect or didn't, and a failed
|
|
345
|
+
// read is not evidence either way.
|
|
346
|
+
const mode = readJournalMode(db);
|
|
209
347
|
if (mode && mode !== 'wal') {
|
|
210
348
|
_networkFsWarnedPaths.add(dbPath);
|
|
211
349
|
process.stderr.write(
|
|
@@ -2612,11 +2612,19 @@ try {
|
|
|
2612
2612
|
const artifactPath = mod.resolveTeamArtifactPath(projectRoot);
|
|
2613
2613
|
if (artifactPath && existsSync(artifactPath)) {
|
|
2614
2614
|
const report = mod.importTeamArtifact({ projectRoot, artifactPath });
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
);
|
|
2615
|
+
// Corrections and deletions are changes the user needs told about just
|
|
2616
|
+
// as much as inserts (#1463) — reporting only `imported` is what let
|
|
2617
|
+
// the additive bug sit unnoticed.
|
|
2618
|
+
const changed =
|
|
2619
|
+
(report?.imported ?? 0) + (report?.updated ?? 0) + (report?.deleted ?? 0) + (report?.resurrected ?? 0);
|
|
2620
|
+
if (changed > 0) {
|
|
2621
|
+
const detail = [
|
|
2622
|
+
report.imported > 0 ? `${plural(report.imported, 'shared learning')} imported` : null,
|
|
2623
|
+
report.updated > 0 ? `${report.updated} corrected` : null,
|
|
2624
|
+
report.deleted > 0 ? `${report.deleted} retired` : null,
|
|
2625
|
+
report.resurrected > 0 ? `${report.resurrected} restored` : null,
|
|
2626
|
+
].filter(Boolean).join(', ');
|
|
2627
|
+
emitMutation('merged team learnings', `${detail} from the git-tracked team artifact`);
|
|
2620
2628
|
}
|
|
2621
2629
|
}
|
|
2622
2630
|
}
|
|
@@ -56,7 +56,7 @@ const startCommand = {
|
|
|
56
56
|
{ name: 'max-cpu-load', type: 'string', description: 'Override maxCpuLoad resource threshold (e.g. 4.0)' },
|
|
57
57
|
{ name: 'min-free-memory', type: 'string', description: 'Override minFreeMemoryPercent resource threshold (e.g. 15)' },
|
|
58
58
|
{ name: 'dashboard-port', type: 'string', description: `Dashboard HTTP port (default: ${DEFAULT_DASHBOARD_PORT})` },
|
|
59
|
-
{ name: '
|
|
59
|
+
{ name: 'dashboard', type: 'boolean', default: true, description: 'Dashboard HTTP server (--no-dashboard to disable)' },
|
|
60
60
|
],
|
|
61
61
|
examples: [
|
|
62
62
|
{ command: 'flo daemon start', description: 'Start daemon in background (default)' },
|
|
@@ -67,7 +67,10 @@ const startCommand = {
|
|
|
67
67
|
action: async (ctx) => {
|
|
68
68
|
const quiet = ctx.flags.quiet;
|
|
69
69
|
const foreground = ctx.flags.foreground;
|
|
70
|
-
|
|
70
|
+
// `--no-dashboard` parses to `dashboard = false`; there has never been a
|
|
71
|
+
// `noDashboard` flag for the parser to set (#1474). The internal name stays
|
|
72
|
+
// negative because it is threaded through the start/attach helpers below.
|
|
73
|
+
const noDashboard = ctx.flags.dashboard === false;
|
|
71
74
|
const rawDashboardPort = ctx.flags.dashboardPort;
|
|
72
75
|
// #1315 — the shared chokepoint. Every daemon-start path lands here:
|
|
73
76
|
// `maybeAutoStartDaemon`, the session-start launcher, bin/hooks.mjs, the
|
|
@@ -525,7 +525,11 @@ const epicCommand = {
|
|
|
525
525
|
return { success: false, message: 'Usage: flo epic <issue-number> [--strategy] [--no-merge] [--verbose] [--dry-run]' };
|
|
526
526
|
}
|
|
527
527
|
const dryRun = ctx.flags.dryRun === true;
|
|
528
|
-
|
|
528
|
+
// `--no-merge` parses to `merge = false`; there has never been a
|
|
529
|
+
// `noMerge` key, so this read was always undefined and the documented
|
|
530
|
+
// alias silently did nothing — an epic asked for single-branch ran
|
|
531
|
+
// auto-merge instead (#1474).
|
|
532
|
+
const noMerge = ctx.flags.merge === false;
|
|
529
533
|
const verbose = ctx.flags['verbose'] === true;
|
|
530
534
|
const strategyFlag = ctx.flags['strategy'];
|
|
531
535
|
let strategy = 'single-branch';
|
|
@@ -219,7 +219,9 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
|
|
|
219
219
|
// explicitly set to 'autonomous' via flag. Non-interactive mode is
|
|
220
220
|
// required for headless execution, so --dangerously-skip-permissions
|
|
221
221
|
// is always included — but --allowedTools restricts the blast radius.
|
|
222
|
-
|
|
222
|
+
// `--no-auto-permissions` parses to `autoPermissions = false`; there has
|
|
223
|
+
// never been a `noAutoPermissions` key for the parser to set (#1474).
|
|
224
|
+
const noAutoPerms = flags.autoPermissions === false;
|
|
223
225
|
if (!noAutoPerms) {
|
|
224
226
|
const permLevel = flags.permissionLevel ?? 'elevated';
|
|
225
227
|
const resolved = resolvePermissions(permLevel);
|
|
@@ -480,10 +482,10 @@ const spawnCommand = {
|
|
|
480
482
|
default: 'elevated'
|
|
481
483
|
},
|
|
482
484
|
{
|
|
483
|
-
name: '
|
|
484
|
-
description: '
|
|
485
|
+
name: 'auto-permissions',
|
|
486
|
+
description: 'Automatic permission handling (--no-auto-permissions to prompt for each action)',
|
|
485
487
|
type: 'boolean',
|
|
486
|
-
default:
|
|
488
|
+
default: true
|
|
487
489
|
},
|
|
488
490
|
{
|
|
489
491
|
name: 'dry-run',
|
|
@@ -2008,10 +2008,10 @@ const coverageRouteCommand = {
|
|
|
2008
2008
|
default: 80
|
|
2009
2009
|
},
|
|
2010
2010
|
{
|
|
2011
|
-
name: '
|
|
2012
|
-
description: '
|
|
2011
|
+
name: 'movector',
|
|
2012
|
+
description: 'movector integration (--no-movector to disable)',
|
|
2013
2013
|
type: 'boolean',
|
|
2014
|
-
default:
|
|
2014
|
+
default: true
|
|
2015
2015
|
}
|
|
2016
2016
|
],
|
|
2017
2017
|
examples: [
|
|
@@ -2021,7 +2021,7 @@ const coverageRouteCommand = {
|
|
|
2021
2021
|
action: async (ctx) => {
|
|
2022
2022
|
const task = ctx.args[0] || ctx.flags.task;
|
|
2023
2023
|
const threshold = ctx.flags.threshold || 80;
|
|
2024
|
-
const useNativeBackend =
|
|
2024
|
+
const useNativeBackend = ctx.flags.movector !== false;
|
|
2025
2025
|
if (!task) {
|
|
2026
2026
|
output.printError('Task description is required. Use --task or -t flag.');
|
|
2027
2027
|
return { success: false, exitCode: 1 };
|
|
@@ -2506,10 +2506,10 @@ const statuslineCommand = {
|
|
|
2506
2506
|
default: false
|
|
2507
2507
|
},
|
|
2508
2508
|
{
|
|
2509
|
-
name: '
|
|
2510
|
-
description: '
|
|
2509
|
+
name: 'color',
|
|
2510
|
+
description: 'ANSI colors (--no-color to disable)',
|
|
2511
2511
|
type: 'boolean',
|
|
2512
|
-
default:
|
|
2512
|
+
default: true
|
|
2513
2513
|
}
|
|
2514
2514
|
],
|
|
2515
2515
|
examples: [
|
|
@@ -2740,7 +2740,7 @@ const statuslineCommand = {
|
|
|
2740
2740
|
return { success: true, data: statusData };
|
|
2741
2741
|
}
|
|
2742
2742
|
// Full colored output
|
|
2743
|
-
const noColor = ctx.flags.
|
|
2743
|
+
const noColor = ctx.flags.color === false;
|
|
2744
2744
|
const c = noColor ? {
|
|
2745
2745
|
reset: '', bold: '', dim: '', red: '', green: '', yellow: '', blue: '',
|
|
2746
2746
|
purple: '', cyan: '', brightRed: '', brightGreen: '', brightYellow: '',
|
|
@@ -68,6 +68,11 @@ const commandLoaders = {
|
|
|
68
68
|
epic: () => import('./epic.js'),
|
|
69
69
|
// Spec-Driven Development artifacts (Epic #1269)
|
|
70
70
|
sdd: () => import('./sdd.js'),
|
|
71
|
+
worktree: () => import('./worktree.js'),
|
|
72
|
+
// Alias key, not just `aliases: ['wt']` on the command: lazy commands resolve
|
|
73
|
+
// through `commandLoaders` by name, and an alias declared only on the command
|
|
74
|
+
// object is unreachable until something has already loaded it.
|
|
75
|
+
wt: () => import('./worktree.js'),
|
|
71
76
|
// GitHub Repository Setup
|
|
72
77
|
github: () => import('./github.js'),
|
|
73
78
|
// /flo run ledger + per-run token rollup (#1333).
|