moflo 4.10.11 → 4.10.12

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.
@@ -272,6 +272,12 @@ export async function checkMemoryDatabase() {
272
272
  * - `memory.db` / `memory.db.bak` — stale once `.moflo/moflo.db` exists.
273
273
  * - `q-learning-model.json` / `model-router-state.json` — live router state
274
274
  * that pre-dates the `.moflo/movector/` defaults; migrate, don't delete.
275
+ * - `lora-weights.json` / `moe-weights.json` — LoRA + MoE weights (#1168
276
+ * moved the writers to `.moflo/movector/`).
277
+ * - `ewc-fisher.json` / `sona-patterns.json` — neural runtime state (#1168
278
+ * moved the writers to `.moflo/neural/`).
279
+ * - `state.json` — `flo swarm init` snapshot (#1168 → `.moflo/swarm/`).
280
+ * - `code-map-hash.txt` — `flo memory code-map` cache (#1168 → `.moflo/memory/`).
275
281
  * - `hooks.log` / `background.log` — diagnostic logs the launcher used to
276
282
  * route to `.swarm/`; relocate to `.moflo/logs/`.
277
283
  *
@@ -291,6 +297,12 @@ export async function checkSwarmResidue() {
291
297
  'memory.db.bak',
292
298
  'q-learning-model.json',
293
299
  'model-router-state.json',
300
+ 'lora-weights.json',
301
+ 'moe-weights.json',
302
+ 'ewc-fisher.json',
303
+ 'sona-patterns.json',
304
+ 'state.json',
305
+ 'code-map-hash.txt',
294
306
  'hooks.log',
295
307
  'background.log',
296
308
  ];
@@ -152,10 +152,27 @@ async function fixSwarmLegacyResidue() {
152
152
  allMigrated = false;
153
153
  }
154
154
  }
155
- // (2) router state JSONs — rename into .moflo/movector/.
155
+ // (2) router state + neural state JSONs — rename into .moflo/{movector,neural,swarm,memory}/.
156
+ //
157
+ // q-learning-model.json + model-router-state.json: shipped at #727.
158
+ // lora-weights.json + moe-weights.json: writer relocation in #1168
159
+ // (lora-adapter.ts, moe-router.ts).
160
+ // ewc-fisher.json + sona-patterns.json: writer relocation in #1168
161
+ // (ewc-consolidation.ts, sona-optimizer.ts).
162
+ // state.json + code-map-hash.txt: writer relocation in #1168
163
+ // (commands/swarm.ts, commands/memory.ts).
164
+ const neuralDir = join(moflo, 'neural');
165
+ const swarmStateDir = join(moflo, 'swarm');
166
+ const memoryStateDir = join(moflo, 'memory');
156
167
  const stateFiles = [
157
168
  { name: 'q-learning-model.json', dest: movectorDir },
158
169
  { name: 'model-router-state.json', dest: movectorDir },
170
+ { name: 'lora-weights.json', dest: movectorDir },
171
+ { name: 'moe-weights.json', dest: movectorDir },
172
+ { name: 'ewc-fisher.json', dest: neuralDir },
173
+ { name: 'sona-patterns.json', dest: neuralDir },
174
+ { name: 'state.json', dest: swarmStateDir },
175
+ { name: 'code-map-hash.txt', dest: memoryStateDir },
159
176
  ];
160
177
  for (const { name, dest } of stateFiles) {
161
178
  const src = join(swarmDir, name);
@@ -230,10 +247,11 @@ export async function autoFixCheck(check) {
230
247
  // Map checks to programmatic fixes (not just shell commands)
231
248
  const fixActions = {
232
249
  'Memory Database': async () => {
250
+ // Canonical DB lives at `.moflo/moflo.db`; `initializeMemoryDatabase`
251
+ // creates the parent dir itself. The pre-#1168 fix also `mkdirSync`'d
252
+ // `.swarm/` — vestigial residue that fought the 'Swarm Residue' fix in
253
+ // the same healer pass. Removed.
233
254
  try {
234
- const swarmDir = join(process.cwd(), '.swarm');
235
- if (!existsSync(swarmDir))
236
- mkdirSync(swarmDir, { recursive: true });
237
255
  const { initializeMemoryDatabase } = await import('../memory/memory-initializer.js');
238
256
  const result = await initializeMemoryDatabase({ force: true, verbose: false });
239
257
  return result.success;
@@ -243,12 +261,11 @@ export async function autoFixCheck(check) {
243
261
  }
244
262
  },
245
263
  'Embeddings': async () => {
264
+ // Same fix as Memory Database — ensure the canonical DB exists, then
265
+ // populate embeddings. Pre-#1168 wrote to `.swarm/memory.db` directly,
266
+ // contradicting the post-#727 layout; that branch is removed.
246
267
  try {
247
- const swarmDir = join(process.cwd(), '.swarm');
248
- if (!existsSync(swarmDir))
249
- mkdirSync(swarmDir, { recursive: true });
250
- const dbPath = join(swarmDir, 'memory.db');
251
- if (!existsSync(dbPath)) {
268
+ if (!existsSync(memoryDbPath(findProjectRoot()))) {
252
269
  const { initializeMemoryDatabase } = await import('../memory/memory-initializer.js');
253
270
  await initializeMemoryDatabase({ force: true, verbose: false });
254
271
  }
@@ -9,6 +9,7 @@ import { select, confirm, input } from '../prompt.js';
9
9
  import { callMCPTool, MCPClientError } from '../mcp-client.js';
10
10
  import { openDaemonDatabase } from '../memory/daemon-backend.js';
11
11
  import { errorDetail } from '../shared/utils/error-detail.js';
12
+ import { legacySwarmPath, runtimePath } from '../services/moflo-paths.js';
12
13
  // Memory backends
13
14
  const BACKENDS = [
14
15
  { value: 'agentdb', label: 'AgentDB', hint: 'Vector database with HNSW indexing (150x-12,500x faster)' },
@@ -2129,7 +2130,10 @@ const codeMapCommand = {
2129
2130
  const { execSync } = await import('child_process');
2130
2131
  const { createHash } = await import('crypto');
2131
2132
  const cwd = ctx.cwd || process.cwd();
2132
- const hashCachePath = pathModule.join(cwd, '.swarm', 'code-map-hash.txt');
2133
+ // Post-#1168: canonical at `.moflo/memory/code-map-hash.txt`. Legacy
2134
+ // `.swarm/code-map-hash.txt` is read-only fallback for upgrade scenarios.
2135
+ const hashCachePath = runtimePath('memory', 'code-map-hash.txt');
2136
+ const legacyHashCachePath = legacySwarmPath('code-map-hash.txt');
2133
2137
  output.writeln();
2134
2138
  output.writeln(output.bold('Generating Code Map'));
2135
2139
  output.writeln(output.dim('─'.repeat(50)));
@@ -2168,9 +2172,12 @@ const codeMapCommand = {
2168
2172
  output.writeln(`File list hash: ${currentHash.slice(0, 12)}...`);
2169
2173
  return { success: true };
2170
2174
  }
2171
- // Check if unchanged
2172
- if (!forceRegen && fs.existsSync(hashCachePath)) {
2173
- const cached = fs.readFileSync(hashCachePath, 'utf-8').trim();
2175
+ // Check if unchanged — canonical first, then legacy `.swarm/` fallback.
2176
+ const cachedReadPath = fs.existsSync(hashCachePath)
2177
+ ? hashCachePath
2178
+ : (fs.existsSync(legacyHashCachePath) ? legacyHashCachePath : null);
2179
+ if (!forceRegen && cachedReadPath) {
2180
+ const cached = fs.readFileSync(cachedReadPath, 'utf-8').trim();
2174
2181
  if (cached === currentHash) {
2175
2182
  const { db } = await openDb(cwd);
2176
2183
  const stmt = db.prepare(`SELECT COUNT(*) as cnt FROM memory_entries WHERE namespace = ?`);
@@ -7,14 +7,26 @@ import { select, confirm } from '../prompt.js';
7
7
  import { callMCPTool, MCPClientError } from '../mcp-client.js';
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
10
- import { memoryDbCandidatePaths } from '../services/moflo-paths.js';
10
+ import { LEGACY_SWARM_DIR, memoryDbCandidatePaths, mofloDir } from '../services/moflo-paths.js';
11
+ import { findProjectRoot } from '../services/project-root.js';
11
12
  // Get dynamic swarm status from memory/session files
12
13
  function getSwarmStatus(swarmId) {
13
- const swarmDir = path.join(process.cwd(), '.swarm');
14
+ const projectRoot = findProjectRoot();
15
+ // `.moflo/swarm/state.json` is canonical post-#1168; `.swarm/state.json`
16
+ // is a read-only fallback so a consumer who initialised on an older moflo
17
+ // still sees their swarm. The pre-#1168 agents/tasks JSON probe blocks
18
+ // were removed — no current writer creates those directories, so they
19
+ // always produced 0 counts. The coordinator-backed MCP tools
20
+ // (agent_list / task_list) are the live source of truth.
21
+ const canonicalSwarmDir = path.join(mofloDir(projectRoot), 'swarm');
22
+ const legacySwarmDir = path.join(projectRoot, LEGACY_SWARM_DIR);
14
23
  const sessionDir = path.join(process.cwd(), '.claude', 'sessions');
15
24
  const memoryPaths = memoryDbCandidatePaths(process.cwd());
16
- // Check for active swarm state file
17
- const swarmStateFile = path.join(swarmDir, 'state.json');
25
+ // Check for active swarm state file — canonical first, then legacy.
26
+ let swarmStateFile = path.join(canonicalSwarmDir, 'state.json');
27
+ if (!fs.existsSync(swarmStateFile)) {
28
+ swarmStateFile = path.join(legacySwarmDir, 'state.json');
29
+ }
18
30
  let swarmState = null;
19
31
  if (fs.existsSync(swarmStateFile)) {
20
32
  try {
@@ -24,30 +36,14 @@ function getSwarmStatus(swarmId) {
24
36
  // Ignore parse errors
25
37
  }
26
38
  }
27
- // Count active agents from process files
28
- let activeAgents = 0;
29
- let totalAgents = 0;
30
- const agentsDir = path.join(swarmDir, 'agents');
31
- if (fs.existsSync(agentsDir)) {
32
- try {
33
- const agentFiles = fs.readdirSync(agentsDir).filter(f => f.endsWith('.json'));
34
- totalAgents = agentFiles.length;
35
- for (const file of agentFiles) {
36
- try {
37
- const agent = JSON.parse(fs.readFileSync(path.join(agentsDir, file), 'utf-8'));
38
- if (agent.status === 'active' || agent.status === 'running') {
39
- activeAgents++;
40
- }
41
- }
42
- catch {
43
- // Ignore
44
- }
45
- }
46
- }
47
- catch {
48
- // Ignore
49
- }
50
- }
39
+ // agents/tasks counters: no file-store readers post-#1168. Coordinator
40
+ // MCP tools own the live counts; getSwarmStatus surfaces a static summary
41
+ // of the persisted state file plus session/memory rough indicators.
42
+ const activeAgents = 0;
43
+ const totalAgents = 0;
44
+ const completedTasks = 0;
45
+ const inProgressTasks = 0;
46
+ const pendingTasks = 0;
51
47
  // Get session count
52
48
  let sessionCount = 0;
53
49
  if (fs.existsSync(sessionDir)) {
@@ -71,36 +67,6 @@ function getSwarmStatus(swarmId) {
71
67
  }
72
68
  }
73
69
  }
74
- // Count task files if they exist
75
- let completedTasks = 0;
76
- let inProgressTasks = 0;
77
- let pendingTasks = 0;
78
- const tasksDir = path.join(swarmDir, 'tasks');
79
- if (fs.existsSync(tasksDir)) {
80
- try {
81
- const taskFiles = fs.readdirSync(tasksDir).filter(f => f.endsWith('.json'));
82
- for (const file of taskFiles) {
83
- try {
84
- const task = JSON.parse(fs.readFileSync(path.join(tasksDir, file), 'utf-8'));
85
- if (task.status === 'completed' || task.status === 'done') {
86
- completedTasks++;
87
- }
88
- else if (task.status === 'in_progress' || task.status === 'running') {
89
- inProgressTasks++;
90
- }
91
- else {
92
- pendingTasks++;
93
- }
94
- }
95
- catch {
96
- // Ignore
97
- }
98
- }
99
- }
100
- catch {
101
- // Ignore
102
- }
103
- }
104
70
  // Calculate dynamic progress based on actual state
105
71
  // If no swarm state, show 0%. Otherwise calculate from completed tasks
106
72
  const totalTasks = completedTasks + inProgressTasks + pendingTasks;
@@ -279,8 +245,11 @@ const initCommand = {
279
245
  });
280
246
  output.writeln();
281
247
  output.printSuccess('Swarm initialized successfully');
282
- // Save swarm state locally for status command to read
283
- const swarmDir = path.join(process.cwd(), '.swarm');
248
+ // Save swarm state locally for status command to read. Post-#1168 the
249
+ // canonical home is `<root>/.moflo/swarm/state.json`; the legacy
250
+ // `.swarm/state.json` path is preserved as a read-only fallback in
251
+ // `getSwarmStatus`.
252
+ const swarmDir = path.join(mofloDir(findProjectRoot()), 'swarm');
284
253
  try {
285
254
  if (!fs.existsSync(swarmDir)) {
286
255
  fs.mkdirSync(swarmDir, { recursive: true });
@@ -16,13 +16,15 @@
16
16
  * - Fisher Information Matrix computation from gradient history
17
17
  * - Online EWC updates for streaming patterns
18
18
  * - Selective consolidation based on pattern importance
19
- * - Persistent storage in .swarm/ewc-fisher.json
19
+ * - Persistent storage in .moflo/neural/ewc-fisher.json
20
+ * (legacy fallback read: .swarm/ewc-fisher.json)
20
21
  *
21
22
  * @module v3/cli/memory/ewc-consolidation
22
23
  */
23
24
  import * as fs from 'fs';
24
25
  import * as path from 'path';
25
26
  import { errorDetail } from '../shared/utils/error-detail.js';
27
+ import { legacySwarmPath, runtimePath } from '../services/moflo-paths.js';
26
28
  // ============================================================================
27
29
  // Default Configuration
28
30
  // ============================================================================
@@ -31,7 +33,6 @@ const DEFAULT_EWC_CONFIG = {
31
33
  maxPatterns: 1000,
32
34
  fisherDecayRate: 0.01,
33
35
  importanceThreshold: 0.3,
34
- storagePath: path.join(process.cwd(), '.swarm', 'ewc-fisher.json'),
35
36
  onlineMode: true,
36
37
  dimensions: 384
37
38
  };
@@ -51,7 +52,15 @@ export class EWCConsolidator {
51
52
  consolidationHistory = [];
52
53
  initialized = false;
53
54
  constructor(config) {
54
- this.config = { ...DEFAULT_EWC_CONFIG, ...config };
55
+ // Resolve storagePath lazily here (#1168) — the default routes through
56
+ // findProjectRoot at construct-time, not module-load time. Default-rescue
57
+ // runs *last* so an explicit `storagePath: undefined` falls back to the
58
+ // canonical path instead of leaving the field undefined.
59
+ this.config = {
60
+ ...DEFAULT_EWC_CONFIG,
61
+ ...config,
62
+ storagePath: config?.storagePath ?? runtimePath('neural', 'ewc-fisher.json'),
63
+ };
55
64
  this.globalFisher = new Array(this.config.dimensions).fill(0);
56
65
  }
57
66
  /**
@@ -447,10 +456,17 @@ export class EWCConsolidator {
447
456
  * Load state from disk
448
457
  */
449
458
  async loadFromDisk() {
450
- if (!fs.existsSync(this.config.storagePath)) {
451
- throw new Error('No persisted state found');
459
+ // Canonical path first, then legacy `.swarm/ewc-fisher.json` as a
460
+ // read-only fallback for consumers who upgraded mid-training (#1168).
461
+ let sourcePath = this.config.storagePath;
462
+ if (!fs.existsSync(sourcePath)) {
463
+ const legacy = legacySwarmPath('ewc-fisher.json');
464
+ if (!fs.existsSync(legacy)) {
465
+ throw new Error('No persisted state found');
466
+ }
467
+ sourcePath = legacy;
452
468
  }
453
- const content = fs.readFileSync(this.config.storagePath, 'utf-8');
469
+ const content = fs.readFileSync(sourcePath, 'utf-8');
454
470
  const state = JSON.parse(content);
455
471
  // Validate version
456
472
  if (state.version !== '1.0.0') {
@@ -8,17 +8,18 @@
8
8
  * - Processes trajectory outcomes from the spell-engine trajectory pipeline
9
9
  * - Extracts keywords from tasks for pattern matching
10
10
  * - Maintains learned routing patterns with confidence scoring
11
- * - Persists patterns to .swarm/sona-patterns.json
11
+ * - Persists patterns to .moflo/neural/sona-patterns.json
12
+ * (legacy fallback read: .swarm/sona-patterns.json)
12
13
  * - Integrates with Q-learning router for combined routing
13
14
  *
14
15
  * @module v3/cli/memory/sona-optimizer
15
16
  */
16
17
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
17
- import { dirname, join } from 'path';
18
+ import { dirname, isAbsolute, resolve } from 'path';
19
+ import { legacySwarmPath, runtimePath } from '../services/moflo-paths.js';
18
20
  // ============================================================================
19
21
  // Constants
20
22
  // ============================================================================
21
- const DEFAULT_PERSISTENCE_PATH = '.swarm/sona-patterns.json';
22
23
  const PATTERN_VERSION = '1.0.0';
23
24
  const MIN_CONFIDENCE = 0.1;
24
25
  const MAX_CONFIDENCE = 0.99;
@@ -105,7 +106,12 @@ export class SONAOptimizer {
105
106
  qLearningRouter = null;
106
107
  qLearningEnabled = false;
107
108
  constructor(options) {
108
- this.persistencePath = options?.persistencePath || DEFAULT_PERSISTENCE_PATH;
109
+ // Resolve persistencePath lazily here (#1168). When the caller supplies
110
+ // one we honor it verbatim (may be relative — preserved for existing
111
+ // tests/callers that join against their own cwd). When unset, we route
112
+ // through runtimePath so writes land under `.moflo/neural/` regardless
113
+ // of subprocess cwd.
114
+ this.persistencePath = options?.persistencePath || runtimePath('neural', 'sona-patterns.json');
109
115
  }
110
116
  /**
111
117
  * Initialize the optimizer and load persisted state
@@ -499,9 +505,17 @@ export class SONAOptimizer {
499
505
  */
500
506
  loadFromDisk() {
501
507
  try {
502
- const fullPath = join(process.cwd(), this.persistencePath);
508
+ // Treat absolute persistencePath verbatim (new #1168 default routes
509
+ // through runtimePath → absolute `.moflo/neural/...`); relative paths
510
+ // preserve the pre-#1168 behaviour of resolving against cwd.
511
+ let fullPath = isAbsolute(this.persistencePath)
512
+ ? this.persistencePath
513
+ : resolve(process.cwd(), this.persistencePath);
503
514
  if (!existsSync(fullPath)) {
504
- return false;
515
+ const legacy = legacySwarmPath('sona-patterns.json');
516
+ if (!existsSync(legacy))
517
+ return false;
518
+ fullPath = legacy;
505
519
  }
506
520
  const data = readFileSync(fullPath, 'utf-8');
507
521
  const state = JSON.parse(data);
@@ -536,7 +550,11 @@ export class SONAOptimizer {
536
550
  */
537
551
  saveToDisk() {
538
552
  try {
539
- const fullPath = join(process.cwd(), this.persistencePath);
553
+ // See loadFromDisk: absolute persistencePath is taken verbatim, relative
554
+ // paths resolve against cwd. New #1168 default writes to `.moflo/neural/`.
555
+ const fullPath = isAbsolute(this.persistencePath)
556
+ ? this.persistencePath
557
+ : resolve(process.cwd(), this.persistencePath);
540
558
  const dir = dirname(fullPath);
541
559
  // Ensure directory exists
542
560
  if (!existsSync(dir)) {
@@ -8,7 +8,7 @@
8
8
  * - Rank decomposition (r << d) for memory efficiency
9
9
  * - Additive weight updates: W' = W + BA (where B ∈ R^{d×r}, A ∈ R^{r×k})
10
10
  * - Support for multiple adaptation heads
11
- * - Persistence to .swarm/lora-weights.json
11
+ * - Persistence to .moflo/movector/lora-weights.json (legacy fallback: .swarm/lora-weights.json)
12
12
  *
13
13
  * Memory savings:
14
14
  * - Original: d × k parameters
@@ -18,7 +18,8 @@
18
18
  * @module lora-adapter
19
19
  */
20
20
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
21
- import { dirname, join } from 'path';
21
+ import { dirname } from 'path';
22
+ import { legacySwarmPath, runtimePath } from '../services/moflo-paths.js';
22
23
  // ============================================================================
23
24
  // Types & Constants
24
25
  // ============================================================================
@@ -47,7 +48,6 @@ const DEFAULT_CONFIG = {
47
48
  inputDim: INPUT_DIM,
48
49
  outputDim: OUTPUT_DIM,
49
50
  learningRate: 0.001,
50
- weightsPath: join(process.cwd(), '.swarm', 'lora-weights.json'),
51
51
  enableDropout: true,
52
52
  dropoutProb: 0.1,
53
53
  autoSaveInterval: 50,
@@ -67,7 +67,16 @@ export class LoRAAdapter {
67
67
  lastUpdate = null;
68
68
  updatesSinceLastSave = 0;
69
69
  constructor(config) {
70
- this.config = { ...DEFAULT_CONFIG, ...config };
70
+ // Resolve weightsPath lazily here, not at module load — captures the
71
+ // *current* consumer project root, not the cwd at first import (#1168).
72
+ // Default-rescue runs *last* so an explicit `weightsPath: undefined` from
73
+ // the caller still falls back to the canonical path instead of crashing
74
+ // save/load on an undefined string.
75
+ this.config = {
76
+ ...DEFAULT_CONFIG,
77
+ ...config,
78
+ weightsPath: config?.weightsPath ?? runtimePath('movector', 'lora-weights.json'),
79
+ };
71
80
  this.weights = this.initializeWeights();
72
81
  }
73
82
  /**
@@ -309,10 +318,16 @@ export class LoRAAdapter {
309
318
  */
310
319
  loadWeights() {
311
320
  try {
312
- if (!existsSync(this.config.weightsPath)) {
313
- return false;
321
+ // Canonical path first, then legacy `.swarm/lora-weights.json` as a
322
+ // read-only fallback for consumers who upgraded mid-training (#1168).
323
+ let sourcePath = this.config.weightsPath;
324
+ if (!existsSync(sourcePath)) {
325
+ const legacy = legacySwarmPath('lora-weights.json');
326
+ if (!existsSync(legacy))
327
+ return false;
328
+ sourcePath = legacy;
314
329
  }
315
- const content = readFileSync(this.config.weightsPath, 'utf-8');
330
+ const content = readFileSync(sourcePath, 'utf-8');
316
331
  const data = JSON.parse(content);
317
332
  if (data.version !== 1) {
318
333
  return false;
@@ -6,7 +6,8 @@
6
6
  * - Gating network for soft expert selection (top-k)
7
7
  * - Online weight updates via reward signals
8
8
  * - Load balancing with auxiliary loss
9
- * - Weight persistence to .swarm/moe-weights.json
9
+ * - Weight persistence to .moflo/movector/moe-weights.json
10
+ * (legacy fallback read: .swarm/moe-weights.json)
10
11
  *
11
12
  * Architecture:
12
13
  * - Input: 384-dim task embedding (from ONNX)
@@ -17,6 +18,7 @@
17
18
  */
18
19
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
19
20
  import { dirname } from 'path';
21
+ import { legacySwarmPath, runtimePath } from '../services/moflo-paths.js';
20
22
  /**
21
23
  * Expert names in order (index corresponds to expert slot)
22
24
  */
@@ -43,14 +45,15 @@ export const INPUT_DIM = 384;
43
45
  */
44
46
  export const HIDDEN_DIM = 128;
45
47
  /**
46
- * Default configuration
48
+ * Default configuration. `weightsPath` is overridden lazily in the
49
+ * MoERouter constructor (see #1168) so the path resolves against the
50
+ * consumer's project root, not the module-load cwd.
47
51
  */
48
52
  const DEFAULT_CONFIG = {
49
53
  topK: 2,
50
54
  learningRate: 0.01,
51
55
  temperature: 1.0,
52
56
  loadBalanceCoef: 0.01,
53
- weightsPath: '.swarm/moe-weights.json',
54
57
  autoSaveInterval: 50,
55
58
  enableNoise: true,
56
59
  noiseStd: 0.1,
@@ -202,7 +205,15 @@ export class MoERouter {
202
205
  lastProbs = null;
203
206
  lastSelectedExperts = [];
204
207
  constructor(config = {}) {
205
- this.config = { ...DEFAULT_CONFIG, ...config };
208
+ // Resolve weightsPath lazily here (#1168) — the default routes through
209
+ // findProjectRoot at construct-time, not module-load time. Default-rescue
210
+ // runs *last* so an explicit `weightsPath: undefined` falls back to the
211
+ // canonical path instead of leaving the field undefined.
212
+ this.config = {
213
+ ...DEFAULT_CONFIG,
214
+ ...config,
215
+ weightsPath: config.weightsPath ?? runtimePath('movector', 'moe-weights.json'),
216
+ };
206
217
  // Initialize weights
207
218
  this.W1 = xavierInit(INPUT_DIM, HIDDEN_DIM);
208
219
  this.b1 = new Float32Array(HIDDEN_DIM);
@@ -445,10 +456,15 @@ export class MoERouter {
445
456
  * Load weights from persistence file
446
457
  */
447
458
  async loadWeights(path) {
448
- const weightsPath = path || this.config.weightsPath;
459
+ // Canonical path first, then legacy `.swarm/moe-weights.json` as a
460
+ // read-only fallback for consumers who upgraded mid-training (#1168).
461
+ let weightsPath = path || this.config.weightsPath;
449
462
  try {
450
463
  if (!existsSync(weightsPath)) {
451
- return false;
464
+ const legacy = legacySwarmPath('moe-weights.json');
465
+ if (!existsSync(legacy))
466
+ return false;
467
+ weightsPath = legacy;
452
468
  }
453
469
  const data = readFileSync(weightsPath, 'utf-8');
454
470
  const model = JSON.parse(data);
@@ -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
@@ -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.12';
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.12",
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.11",
99
99
  "tsx": "^4.21.0",
100
100
  "typescript": "^5.9.3",
101
101
  "vitest": "^4.0.0"