moflo 4.10.12 → 4.10.13

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.
@@ -62,6 +62,56 @@ function buildDefaultOptions() {
62
62
  daemonize: false,
63
63
  };
64
64
  }
65
+ /**
66
+ * Best-effort append to the MCP log file. Errors are swallowed — logging must
67
+ * never crash the MCP server. Used to capture server start, project root
68
+ * resolution, and per-request timing so we never repeat the 18-hour
69
+ * diagnostic blind window from #1174.
70
+ *
71
+ * Rotation: when the log exceeds {@link MCP_LOG_ROTATE_BYTES}, rename it to
72
+ * `<logFile>.1` (overwriting any previous rotated file). One rotation level
73
+ * keeps the most recent ~50MB of activity plus the previous ~50MB. A long-
74
+ * running session with heavy MCP traffic can otherwise write hundreds of MB.
75
+ *
76
+ * Cross-platform: uses fs.appendFileSync + fs.mkdirSync({recursive:true}) +
77
+ * fs.renameSync. All three work identically on Windows/macOS/Linux. Windows
78
+ * note: renameSync can fail with EBUSY if the file is open by another
79
+ * process; we use append-only here so no other writer should hold it, but
80
+ * the rename is wrapped in a try/catch so a transient rotation failure can't
81
+ * crash the MCP server (next append succeeds; rotation retries next time).
82
+ */
83
+ const MCP_LOG_ROTATE_BYTES = 50 * 1024 * 1024;
84
+ // Throttle rotation checks: batch spell scenarios can write thousands of
85
+ // MCP requests per session. statSync per append is wasted syscalls — bucket
86
+ // the check every N writes (and always on the very first call so cold-start
87
+ // rotation still fires).
88
+ const MCP_LOG_ROTATE_CHECK_EVERY = 100;
89
+ let mcpAppendsSinceRotateCheck = MCP_LOG_ROTATE_CHECK_EVERY;
90
+ function safeAppendMcpLog(logFile, event) {
91
+ try {
92
+ fs.mkdirSync(path.dirname(logFile), { recursive: true });
93
+ // Rotate before append so the very write that crosses the threshold
94
+ // lands in the fresh file rather than the rotated one.
95
+ if (++mcpAppendsSinceRotateCheck >= MCP_LOG_ROTATE_CHECK_EVERY) {
96
+ mcpAppendsSinceRotateCheck = 0;
97
+ try {
98
+ const stats = fs.statSync(logFile);
99
+ if (stats.size >= MCP_LOG_ROTATE_BYTES) {
100
+ const rotated = `${logFile}.1`;
101
+ try {
102
+ fs.unlinkSync(rotated);
103
+ }
104
+ catch { /* may not exist */ }
105
+ fs.renameSync(logFile, rotated);
106
+ }
107
+ }
108
+ catch { /* file may not exist yet; first write creates it */ }
109
+ }
110
+ const line = JSON.stringify({ ts: new Date().toISOString(), ...event }) + '\n';
111
+ fs.appendFileSync(logFile, line, 'utf-8');
112
+ }
113
+ catch { /* logging must never throw */ }
114
+ }
65
115
  /**
66
116
  * MCP Server Manager
67
117
  *
@@ -253,6 +303,27 @@ export class MCPServerManager extends EventEmitter {
253
303
  sessionId,
254
304
  version: VERSION,
255
305
  }));
306
+ // Persistent log (#1174). The MCP server previously logged only to stderr,
307
+ // which Claude Code drops on the floor unless the user runs `claude
308
+ // --debug`. The 18-hour daemon-island incident took that long to diagnose
309
+ // partly because no on-disk log captured server start, the resolved
310
+ // project root, or the request stream. Default-on JSONL log fixes that.
311
+ const resolvedProjectRoot = findProjectRoot();
312
+ safeAppendMcpLog(this.options.logFile, {
313
+ event: 'server.start',
314
+ sessionId,
315
+ version: VERSION,
316
+ pid: process.pid,
317
+ ppid: process.ppid,
318
+ platform: process.platform,
319
+ arch: process.arch,
320
+ nodeVersion: process.version,
321
+ cwd: process.cwd(),
322
+ projectRoot: resolvedProjectRoot,
323
+ claudeProjectDir: process.env.CLAUDE_PROJECT_DIR || null,
324
+ pidFile: this.options.pidFile,
325
+ logFile: this.options.logFile,
326
+ });
256
327
  // Send server initialization notification
257
328
  console.log(JSON.stringify({
258
329
  jsonrpc: '2.0',
@@ -380,8 +451,16 @@ export class MCPServerManager extends EventEmitter {
380
451
  },
381
452
  },
382
453
  };
383
- case 'tools/list':
454
+ case 'tools/list': {
455
+ const listStart = performance.now();
384
456
  const tools = listMCPTools();
457
+ const durationMs = performance.now() - listStart;
458
+ safeAppendMcpLog(this.options.logFile, {
459
+ event: 'tools/list',
460
+ sessionId,
461
+ count: tools.length,
462
+ durationMs: Math.round(durationMs * 100) / 100,
463
+ });
385
464
  return {
386
465
  jsonrpc: '2.0',
387
466
  id: message.id,
@@ -393,18 +472,32 @@ export class MCPServerManager extends EventEmitter {
393
472
  })),
394
473
  },
395
474
  };
396
- case 'tools/call':
475
+ }
476
+ case 'tools/call': {
397
477
  const toolName = params.name;
398
478
  const toolParams = (params.arguments || {});
399
479
  if (!hasTool(toolName)) {
480
+ safeAppendMcpLog(this.options.logFile, {
481
+ event: 'tools/call.unknown',
482
+ sessionId,
483
+ toolName,
484
+ });
400
485
  return {
401
486
  jsonrpc: '2.0',
402
487
  id: message.id,
403
488
  error: { code: -32601, message: `Tool not found: ${toolName}` },
404
489
  };
405
490
  }
491
+ const callStart = performance.now();
406
492
  try {
407
493
  const result = await callMCPTool(toolName, toolParams, { sessionId });
494
+ const durationMs = performance.now() - callStart;
495
+ safeAppendMcpLog(this.options.logFile, {
496
+ event: 'tools/call.ok',
497
+ sessionId,
498
+ toolName,
499
+ durationMs: Math.round(durationMs * 100) / 100,
500
+ });
408
501
  return {
409
502
  jsonrpc: '2.0',
410
503
  id: message.id,
@@ -412,6 +505,14 @@ export class MCPServerManager extends EventEmitter {
412
505
  };
413
506
  }
414
507
  catch (error) {
508
+ const durationMs = performance.now() - callStart;
509
+ safeAppendMcpLog(this.options.logFile, {
510
+ event: 'tools/call.error',
511
+ sessionId,
512
+ toolName,
513
+ durationMs: Math.round(durationMs * 100) / 100,
514
+ error: error instanceof Error ? error.message : 'Tool execution failed',
515
+ });
415
516
  return {
416
517
  jsonrpc: '2.0',
417
518
  id: message.id,
@@ -421,6 +522,7 @@ export class MCPServerManager extends EventEmitter {
421
522
  },
422
523
  };
423
524
  }
525
+ }
424
526
  case 'notifications/initialized':
425
527
  // Client notification - no response needed
426
528
  console.error(`[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Client initialized`);
@@ -56,7 +56,9 @@ export function getReferenceHookBlock() {
56
56
  { matcher: '^(Glob|Grep)$', hooks: [gateHook('check-before-scan', 3000)] },
57
57
  { matcher: '^Read$', hooks: [gateHook('check-before-read', 3000)] },
58
58
  {
59
- matcher: '^Bash$',
59
+ // #1171 — widened to cover `PowerShell` tool; without this PS-tool
60
+ // calls bypass the dangerous/pr/memory gates on Windows.
61
+ matcher: '^(Bash|PowerShell)$',
60
62
  hooks: [
61
63
  gateHook('check-dangerous-command', 2000),
62
64
  gateHook('check-before-pr', 2000),
@@ -78,7 +80,8 @@ export function getReferenceHookBlock() {
78
80
  { matcher: '^TaskCreate$', hooks: [gateCjs('record-task-created', 2000)] },
79
81
  {
80
82
  // #1132 — check-bash-memory moved to PreToolUse (above).
81
- matcher: '^Bash$',
83
+ // #1171 — widened to cover `PowerShell` tool.
84
+ matcher: '^(Bash|PowerShell)$',
82
85
  hooks: [gateHook('record-test-run', 2000)],
83
86
  },
84
87
  { matcher: '^Skill$', hooks: [gateHook('record-skill-run', 2000)] },
@@ -45,8 +45,10 @@ export const REQUIRED_HOOK_WIRING = [
45
45
  export const HOOK_ENTRY_MAP = {
46
46
  'check-before-scan': { event: 'PreToolUse', matcher: '^(Glob|Grep)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-before-scan', timeout: 3000 } },
47
47
  'check-before-read': { event: 'PreToolUse', matcher: '^Read$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-before-read', timeout: 3000 } },
48
- 'check-dangerous-command': { event: 'PreToolUse', matcher: '^Bash$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-dangerous-command', timeout: 2000 } },
49
- 'check-before-pr': { event: 'PreToolUse', matcher: '^Bash$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-before-pr', timeout: 2000 } },
48
+ // #1171 matchers widened to cover `PowerShell` tool; the gate logic was
49
+ // always shell-agnostic but the matcher was Bash-anchored, leaving a bypass.
50
+ 'check-dangerous-command': { event: 'PreToolUse', matcher: '^(Bash|PowerShell)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-dangerous-command', timeout: 2000 } },
51
+ 'check-before-pr': { event: 'PreToolUse', matcher: '^(Bash|PowerShell)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-before-pr', timeout: 2000 } },
50
52
  'record-task-created': { event: 'PostToolUse', matcher: '^TaskCreate$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate.cjs" record-task-created', timeout: 2000 } },
51
53
  // record-memory-searched MUST go through gate-hook.mjs (not gate.cjs directly)
52
54
  // — the wrapper forwards Claude Code's session_id as HOOK_SESSION_ID, which
@@ -58,8 +60,10 @@ export const HOOK_ENTRY_MAP = {
58
60
  'record-memory-searched': { event: 'PostToolUse', matcher: '^mcp__moflo__memory_(search|retrieve|list|stats|store)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" record-memory-searched', timeout: 3000 } },
59
61
  'check-task-transition': { event: 'PostToolUse', matcher: '^TaskUpdate$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate.cjs" check-task-transition', timeout: 2000 } },
60
62
  'record-learnings-stored': { event: 'PostToolUse', matcher: '^mcp__moflo__memory_store$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate.cjs" record-learnings-stored', timeout: 2000 } },
61
- 'check-bash-memory': { event: 'PostToolUse', matcher: '^Bash$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-bash-memory', timeout: 2000 } },
62
- 'record-test-run': { event: 'PostToolUse', matcher: '^Bash$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" record-test-run', timeout: 2000 } },
63
+ // #1171 widened to ^(Bash|PowerShell)$ so PS reads / PS-invoked tests credit
64
+ // the same gates as Bash. Name kept as `check-bash-memory` for backwards compat.
65
+ 'check-bash-memory': { event: 'PostToolUse', matcher: '^(Bash|PowerShell)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" check-bash-memory', timeout: 2000 } },
66
+ 'record-test-run': { event: 'PostToolUse', matcher: '^(Bash|PowerShell)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" record-test-run', timeout: 2000 } },
63
67
  'record-skill-run': { event: 'PostToolUse', matcher: '^Skill$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" record-skill-run', timeout: 2000 } },
64
68
  'reset-edit-gates': { event: 'PostToolUse', matcher: '^(Write|Edit|MultiEdit)$', hook: { type: 'command', command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/gate-hook.mjs" reset-edit-gates', timeout: 2000 } },
65
69
  // #931 — Agent-time advisory; never blocks. Pulled the TaskCreate REMINDER
@@ -165,6 +169,36 @@ export const MATCHER_REWRITE_RULES = [
165
169
  to: '^mcp__moflo__memory_(search|retrieve|list|stats|store)$',
166
170
  cmdContains: 'record-memory-searched',
167
171
  },
172
+ // Issue #1171 — widen Bash-only matchers to cover the dedicated `PowerShell`
173
+ // tool Claude Code exposes on Windows. The gate logic itself was already
174
+ // shell-agnostic (gate.cjs READ_LIKE_BASH_RE matched `Get-Content`/`Select-String`/etc.)
175
+ // but a Bash-anchored matcher meant PS-tool calls never reached the gate.
176
+ // One rewrite per gate command keeps the `cmdContains` guard precise, so an
177
+ // unrelated user-customised `^Bash$` block doesn't get widened.
178
+ {
179
+ name: '#1171: widen check-dangerous-command matcher to PowerShell',
180
+ from: '^Bash$',
181
+ to: '^(Bash|PowerShell)$',
182
+ cmdContains: 'check-dangerous-command',
183
+ },
184
+ {
185
+ name: '#1171: widen check-before-pr matcher to PowerShell',
186
+ from: '^Bash$',
187
+ to: '^(Bash|PowerShell)$',
188
+ cmdContains: 'check-before-pr',
189
+ },
190
+ {
191
+ name: '#1171: widen check-bash-memory matcher to PowerShell',
192
+ from: '^Bash$',
193
+ to: '^(Bash|PowerShell)$',
194
+ cmdContains: 'check-bash-memory',
195
+ },
196
+ {
197
+ name: '#1171: widen record-test-run matcher to PowerShell',
198
+ from: '^Bash$',
199
+ to: '^(Bash|PowerShell)$',
200
+ cmdContains: 'record-test-run',
201
+ },
168
202
  ];
169
203
  /**
170
204
  * Apply HOOK_REWRITE_RULES to every hook command in `settings.hooks.*`.
@@ -104,4 +104,20 @@ export function memoryDbCandidatePaths(projectRoot) {
104
104
  join(projectRoot, '.claude', LEGACY_MEMORY_DB_FILE),
105
105
  ];
106
106
  }
107
+ /**
108
+ * Common skip-list for any walk that enumerates a project's children looking
109
+ * for moflo state. Shared by `bin/session-start-launcher.mjs` (depth-1 walk)
110
+ * and `doctor-checks-config.ts` (depth-5 BFS) so the two can't silently
111
+ * diverge.
112
+ *
113
+ * Twin of `bin/lib/moflo-paths.mjs:COMMON_WALK_SKIP_NAMES`. Matched
114
+ * case-insensitively at every call site — Windows NTFS + macOS APFS are
115
+ * case-insensitive by default.
116
+ */
117
+ export const COMMON_WALK_SKIP_NAMES = new Set([
118
+ 'node_modules', '.git', '.svn', '.hg',
119
+ 'dist', 'build', 'out', 'target', '.next', '.nuxt', '.cache',
120
+ 'coverage', '.idea', '.vscode', '.turbo', '.svelte-kit',
121
+ 'vendor', '__pycache__', '.venv', 'venv', '.tox',
122
+ ]);
107
123
  //# sourceMappingURL=moflo-paths.js.map
@@ -8,32 +8,73 @@
8
8
  * resolve through this single algorithm or its JS twin — otherwise different
9
9
  * writers land on different DBs and the bridge reads stale data.
10
10
  *
11
- * Algorithm (#1057) — two-pass walk so memory markers always win:
11
+ * Algorithm (#1057, #1174) — three-pass walk so memory markers always win
12
+ * across the ENTIRE ancestor chain (not just at the first level they appear):
12
13
  * 1. `process.env.CLAUDE_PROJECT_DIR`, if set (Claude Code / explicit override).
13
- * 2. **High-priority pass.** Walk from `opts.cwd ?? process.cwd()` up to the
14
- * filesystem root, looking at each level for (in order):
15
- * a. `<dir>/.moflo/moflo.db` canonical memory DB marker
16
- * b. `<dir>/.swarm/memory.db` — legacy memory DB marker (pre-#727)
17
- * c. `<dir>/CLAUDE.md` AND `<dir>/package.json` project marker pair
18
- * If anything matches, return that dir. `node_modules` segments are
19
- * skipped (npx run can land cwd inside one).
20
- * 3. **Low-priority pass.** Walk again from cwd up to root looking for:
21
- * d. `<dir>/package.json` generic project marker
22
- * e. `<dir>/.git` — git repo marker
23
- * Return the first match.
24
- * 4. Fall back to `opts.cwd ?? process.cwd()`.
14
+ * 2. **Pass A memory markers (topmost wins).** Walk from
15
+ * `opts.cwd ?? process.cwd()` up to the filesystem root, collecting EVERY
16
+ * level that has `.moflo/moflo.db` OR `.swarm/memory.db`. Return the
17
+ * topmost (highest ancestor) match. This is the #1174 fix — pre-#1174 the
18
+ * walk stopped at the nearest hit, fragmenting monorepos into daemon
19
+ * islands.
20
+ * 3. **Pass B project marker pair (nearest wins).** Only reached when no
21
+ * moflo state exists anywhere up the tree. Walk again looking for
22
+ * `<dir>/CLAUDE.md` AND `<dir>/package.json` at the same level; return
23
+ * the nearest match.
24
+ * 4. **Pass C — bare project markers (nearest wins).** Walk again looking
25
+ * for `<dir>/package.json` OR `<dir>/.git`; return the nearest match.
26
+ * 5. Fall back to `opts.cwd ?? process.cwd()`.
25
27
  *
26
- * Why two passes? An upstream `.moflo/moflo.db` MUST win over a nested
27
- * `package.json` (monorepo sub-package case) — otherwise the writer lands on
28
- * a different DB than the bridge. Doing it in a single pass with bare
29
- * `package.json` as a per-level marker would short-circuit at the nearest
30
- * package.json before ever seeing the upstream memory marker.
28
+ * `node_modules` segments are always skipped (npx run can land cwd inside one).
29
+ *
30
+ * Why topmost (Pass A)? When a monorepo has nested `.moflo/moflo.db` directories
31
+ * — typically because `flo init` was run from a subworkspace before #1174 the
32
+ * MCP server, daemon, CLI, and gate hooks ALL must agree on a single anchor.
33
+ * Topmost wins means the root daemon is canonical; sub-daemons become
34
+ * detectable residue that `flo doctor --fix` archives. Nearest-wins fragments
35
+ * state silently because every cwd resolves to a different anchor.
36
+ *
37
+ * Why nearest (Pass B/C)? Pass B/C only fires when there's no moflo state at
38
+ * all. In a fresh checkout the user expects `flo init` to anchor at the
39
+ * project they're in, not at some ancestor `.git`/`package.json` directory.
31
40
  *
32
41
  * Story #229 history: this function was first extracted from workflow-tools.ts;
33
- * #1057 brought it into alignment with bridge-core.getProjectRoot().
42
+ * #1057 brought it into alignment with bridge-core.getProjectRoot(); #1174
43
+ * changed Pass A from nearest-wins to topmost-wins to fix monorepo daemon
44
+ * fragmentation.
34
45
  */
35
46
  import { existsSync } from 'node:fs';
36
47
  import { resolve, dirname, parse, join, basename } from 'node:path';
48
+ /**
49
+ * Walk strictly upward from `dir` (exclusive) and return the nearest ancestor
50
+ * that has `.moflo/moflo.db`, or `null` if none exists below the filesystem
51
+ * root.
52
+ *
53
+ * Used by `flo init` and the session-start launcher to detect nested-.moflo
54
+ * situations (#1174). Post-resolver-fix `findProjectRoot` returns the topmost
55
+ * memory marker, so encountering an ancestor here means either:
56
+ * 1. `CLAUDE_PROJECT_DIR` explicitly overrode to a sub-directory
57
+ * (legitimate user action — log a warning but don't refuse), or
58
+ * 2. The caller is operating on a directory that's about to become a new
59
+ * nested .moflo/ island (e.g. `flo init` in a sub-workspace).
60
+ *
61
+ * Algorithmic twin of `bin/lib/moflo-paths.mjs:findAncestorMofloRoot()`.
62
+ */
63
+ export function findAncestorMofloRoot(dir) {
64
+ const start = resolve(dir);
65
+ const fsRoot = parse(start).root;
66
+ let cursor = dirname(start);
67
+ while (cursor !== fsRoot) {
68
+ if (existsSync(join(cursor, '.moflo', 'moflo.db'))) {
69
+ return cursor;
70
+ }
71
+ const parent = dirname(cursor);
72
+ if (parent === cursor)
73
+ break;
74
+ cursor = parent;
75
+ }
76
+ return null;
77
+ }
37
78
  export function findProjectRoot(opts) {
38
79
  const honorEnv = opts?.honorEnv !== false;
39
80
  if (honorEnv && process.env.CLAUDE_PROJECT_DIR) {
@@ -42,17 +83,35 @@ export function findProjectRoot(opts) {
42
83
  const startDir = opts?.cwd ?? process.cwd();
43
84
  const start = resolve(startDir);
44
85
  const fsRoot = parse(start).root;
45
- // High-priority pass: memory markers + CLAUDE.md/package.json pair.
86
+ // Pass A memory markers, topmost wins (#1174).
87
+ // Collect every ancestor with `.moflo/moflo.db` or `.swarm/memory.db`, then
88
+ // return the highest one. Guarantees the root daemon is canonical in a
89
+ // monorepo with nested .moflo/ residue.
90
+ let topmostMemoryMarker = null;
46
91
  let dir = start;
47
92
  while (dir !== fsRoot) {
48
93
  if (basename(dir) === 'node_modules') {
49
94
  dir = dirname(dir);
50
95
  continue;
51
96
  }
52
- if (existsSync(join(dir, '.moflo', 'moflo.db')))
53
- return dir;
54
- if (existsSync(join(dir, '.swarm', 'memory.db')))
55
- return dir;
97
+ if (existsSync(join(dir, '.moflo', 'moflo.db')) || existsSync(join(dir, '.swarm', 'memory.db'))) {
98
+ topmostMemoryMarker = dir;
99
+ }
100
+ const parent = dirname(dir);
101
+ if (parent === dir)
102
+ break;
103
+ dir = parent;
104
+ }
105
+ if (topmostMemoryMarker)
106
+ return topmostMemoryMarker;
107
+ // Pass B — project marker pair, nearest wins. Only reached when no moflo
108
+ // state exists anywhere up the tree.
109
+ dir = start;
110
+ while (dir !== fsRoot) {
111
+ if (basename(dir) === 'node_modules') {
112
+ dir = dirname(dir);
113
+ continue;
114
+ }
56
115
  if (existsSync(join(dir, 'CLAUDE.md')) && existsSync(join(dir, 'package.json'))) {
57
116
  return dir;
58
117
  }
@@ -61,7 +120,7 @@ export function findProjectRoot(opts) {
61
120
  break;
62
121
  dir = parent;
63
122
  }
64
- // Low-priority pass: bare package.json or .git.
123
+ // Pass C bare package.json or .git, nearest wins.
65
124
  dir = start;
66
125
  while (dir !== fsRoot) {
67
126
  if (basename(dir) === 'node_modules') {
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.10.12';
5
+ export const VERSION = '4.10.13';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.10.12",
3
+ "version": "4.10.13",
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",
@@ -95,7 +95,7 @@
95
95
  "@typescript-eslint/eslint-plugin": "^7.18.0",
96
96
  "@typescript-eslint/parser": "^7.18.0",
97
97
  "eslint": "^8.0.0",
98
- "moflo": "^4.10.11",
98
+ "moflo": "^4.10.12",
99
99
  "tsx": "^4.21.0",
100
100
  "typescript": "^5.9.3",
101
101
  "vitest": "^4.0.0"