moflo 4.10.11 → 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.
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import { homedir } from 'node:os';
23
23
  import { join } from 'node:path';
24
+ import { findProjectRoot } from './project-root.js';
24
25
  export const MOFLO_DIR = '.moflo';
25
26
  /** Canonical memory DB filename (post-#727). Lives at `<root>/.moflo/moflo.db`. */
26
27
  export const MEMORY_DB_FILE = 'moflo.db';
@@ -68,6 +69,25 @@ export function legacyHnswIndexPath(projectRoot) {
68
69
  export function legacyMemoryDbBakPath(projectRoot) {
69
70
  return join(projectRoot, LEGACY_SWARM_DIR, `${LEGACY_MEMORY_DB_FILE}${LEGACY_MEMORY_DB_BAK_SUFFIX}`);
70
71
  }
72
+ /**
73
+ * Resolve a runtime-state path under `.moflo/<subdir>/<filename>` at the
74
+ * project root. Lazy by design — every call routes through `findProjectRoot()`
75
+ * so the path never bakes in `process.cwd()` at module-load time (#1168 bug
76
+ * class). Use this for any persisted runtime state the daemon, MCP server,
77
+ * or neural runtime writes (weights, fisher matrix, routing patterns, etc.).
78
+ */
79
+ export function runtimePath(subdir, filename) {
80
+ return join(mofloDir(findProjectRoot()), subdir, filename);
81
+ }
82
+ /**
83
+ * Legacy `.swarm/<filename>` path at the project root. Read-only fallback for
84
+ * consumers who saved state under the pre-#1168 location; never written by
85
+ * production code (enforced by the drift-guard in
86
+ * `published-package-drift-guard.test.ts`).
87
+ */
88
+ export function legacySwarmPath(filename) {
89
+ return join(findProjectRoot(), LEGACY_SWARM_DIR, filename);
90
+ }
71
91
  /**
72
92
  * Memory-DB probe order used by every reader that does best-effort detection
73
93
  * (statusline, doctor, swarm status, hooks aggregator). Canonical first so
@@ -84,4 +104,20 @@ export function memoryDbCandidatePaths(projectRoot) {
84
104
  join(projectRoot, '.claude', LEGACY_MEMORY_DB_FILE),
85
105
  ];
86
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
+ ]);
87
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.11';
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.11",
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.10",
98
+ "moflo": "^4.10.12",
99
99
  "tsx": "^4.21.0",
100
100
  "typescript": "^5.9.3",
101
101
  "vitest": "^4.0.0"