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.
Files changed (43) hide show
  1. package/.claude/guidance/shipped/moflo-cli-reference.md +45 -1
  2. package/.claude/guidance/shipped/moflo-cross-install-memory-sharing.md +7 -2
  3. package/.claude/guidance/shipped/moflo-skills-reference.md +2 -0
  4. package/.claude/skills/fl/phases.md +51 -17
  5. package/.claude/skills/optimize-learnings/SKILL.md +220 -0
  6. package/README.md +95 -1
  7. package/bin/lib/get-backend.mjs +150 -12
  8. package/bin/lib/skill-categories.mjs +1 -0
  9. package/bin/session-start-launcher.mjs +13 -5
  10. package/dist/src/cli/commands/daemon.js +5 -2
  11. package/dist/src/cli/commands/epic.js +5 -1
  12. package/dist/src/cli/commands/hive-mind.js +6 -4
  13. package/dist/src/cli/commands/hooks.js +8 -8
  14. package/dist/src/cli/commands/index.js +5 -0
  15. package/dist/src/cli/commands/memory-audit-learnings.js +587 -0
  16. package/dist/src/cli/commands/memory.js +71 -10
  17. package/dist/src/cli/commands/spell-schedule.js +5 -3
  18. package/dist/src/cli/commands/worktree.js +408 -0
  19. package/dist/src/cli/config/moflo-config.js +57 -0
  20. package/dist/src/cli/index.js +4 -2
  21. package/dist/src/cli/init/executor.js +1 -0
  22. package/dist/src/cli/mcp-tools/memory-admin-tools.js +46 -8
  23. package/dist/src/cli/mcp-tools/moflodb-tools.js +30 -6
  24. package/dist/src/cli/memory/bridge-entries.js +157 -9
  25. package/dist/src/cli/memory/controllers/batch-operations.js +7 -2
  26. package/dist/src/cli/memory/daemon-backend.js +152 -11
  27. package/dist/src/cli/memory/entries-read.js +47 -2
  28. package/dist/src/cli/memory/entries-write.js +73 -10
  29. package/dist/src/cli/memory/hnsw-singleton.js +112 -9
  30. package/dist/src/cli/memory/learnings-audit.js +420 -0
  31. package/dist/src/cli/memory/learnings-dead-paths.js +202 -0
  32. package/dist/src/cli/memory/learnings-tree.js +187 -0
  33. package/dist/src/cli/memory/memory-bridge.js +37 -27
  34. package/dist/src/cli/memory/tool-call-markup.js +218 -0
  35. package/dist/src/cli/parser.js +7 -3
  36. package/dist/src/cli/services/cherry-pick-learnings.js +9 -3
  37. package/dist/src/cli/services/durable-reconcile.js +161 -0
  38. package/dist/src/cli/services/durable-store-io.js +291 -0
  39. package/dist/src/cli/services/durable-sync.js +159 -24
  40. package/dist/src/cli/services/team-artifact-sync.js +462 -163
  41. package/dist/src/cli/services/worktree-provision.js +400 -0
  42. package/dist/src/cli/version.js +1 -1
  43. package/package.json +2 -2
@@ -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('PRAGMA busy_timeout = 15000');
167
- db.exec('PRAGMA journal_mode = WAL');
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
- let mode;
200
- try {
201
- const stmt = db.prepare('PRAGMA journal_mode');
202
- const row = stmt.get();
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(
@@ -56,6 +56,7 @@ export const SKILL_CATEGORIES_MAP = {
56
56
  'vector-search',
57
57
  'memory-worktree',
58
58
  'memory-team',
59
+ 'optimize-learnings',
59
60
  ],
60
61
  spells: [
61
62
  'spell-builder',
@@ -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
- if (report?.imported > 0) {
2616
- emitMutation(
2617
- 'merged team learnings',
2618
- `${plural(report.imported, 'shared learning')} imported from the git-tracked team artifact`,
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: 'no-dashboard', type: 'boolean', description: 'Disable the dashboard HTTP server' },
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
- const noDashboard = ctx.flags.noDashboard;
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
- const noMerge = ctx.flags.noMerge === true;
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
- const noAutoPerms = flags.noAutoPermissions;
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: 'no-auto-permissions',
484
- description: 'Disable automatic permission handling (Claude will prompt for each action)',
485
+ name: 'auto-permissions',
486
+ description: 'Automatic permission handling (--no-auto-permissions to prompt for each action)',
485
487
  type: 'boolean',
486
- default: false
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: 'no-movector',
2012
- description: 'Disable movector integration',
2011
+ name: 'movector',
2012
+ description: 'movector integration (--no-movector to disable)',
2013
2013
  type: 'boolean',
2014
- default: false
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 = !ctx.flags.noMovector;
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: 'no-color',
2510
- description: 'Disable ANSI colors',
2509
+ name: 'color',
2510
+ description: 'ANSI colors (--no-color to disable)',
2511
2511
  type: 'boolean',
2512
- default: false
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.noColor;
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).