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.
@@ -29,11 +29,11 @@ function mofloSection() {
29
29
 
30
30
  ### FIRST ACTION ON EVERY PROMPT: Search Memory
31
31
 
32
- Your first tool call MUST be \`mcp__moflo__memory_search\` — before any Glob/Grep/Read. Search \`guidance\`, \`patterns\`, and \`learnings\` every prompt; add \`code-map\` when navigating code, \`tests\` when looking for test inventory or coverage. When the user says "remember this", call \`mcp__moflo__memory_store\` with namespace \`learnings\`.
32
+ Your first tool call MUST be \`mcp__moflo__memory_search\` — before any Glob/Grep/Read. Pick the namespace by question shape: \`code-map\` for "where is symbol X defined", \`tests\` for "what tests cover Y", \`patterns\` for "what's our pattern for Z", \`guidance\` for project rules, \`learnings\` for "did we hit this before". Pivot on the bare symbol/keyword (not a natural-language question), and trust similarity ≥ 0.80 as a confident hit. When the user says "remember this", call \`mcp__moflo__memory_store\` with namespace \`learnings\`.
33
33
 
34
34
  ### Traverse chunks, don't bulk-retrieve
35
35
 
36
- Search results carry a compact \`navigation\` crumb (parentDoc, prev/next, chunkTitle). For adjacent/sibling/hierarchical context use \`mcp__moflo__memory_get_neighbors\`; for full chunk content use \`mcp__moflo__memory_retrieve\`; \`Read\` the source doc only via \`parentPath\` when truly needed. Full protocol: \`.claude/guidance/moflo-memory-protocol.md\`.
36
+ Search results carry a compact \`navigation\` crumb (parentDoc, prev/next, chunkTitle). For adjacent/sibling/hierarchical context use \`mcp__moflo__memory_get_neighbors\`; for full chunk content use \`mcp__moflo__memory_retrieve\`; \`Read\` the source doc only via \`parentPath\` when truly needed. Full protocol + worked examples per namespace: \`.claude/guidance/moflo-memory-protocol.md\`.
37
37
 
38
38
  ### Auto-enforced gates
39
39
 
@@ -48,6 +48,10 @@ Prefer MCP (\`mcp__moflo__*\` — memory, swarm, agent, task, hooks, hive-mind,
48
48
 
49
49
  After \`npm install\` touches moflo, check \`.moflo/restart-pending.json\` — if present, surface its \`message\` field to the user verbatim, then delete the file. (Claude Code only loads new hooks/MCP/launcher at session start.)
50
50
 
51
+ ### Monorepos
52
+
53
+ Moflo state lives at the monorepo root \`.moflo/\` — never run \`flo init\` inside a sub-workspace of an existing moflo project, or the MCP server and CLI silently bind to different daemons (issue #1174).
54
+
51
55
  ### Full Reference
52
56
 
53
57
  - Universal agent rules (memory protocol, git/PR conventions, file org, build/test): \`.claude/guidance/moflo-agent-rules.md\`
@@ -276,11 +276,28 @@ var config = loadGateConfig();
276
276
  var command = process.argv[2];
277
277
 
278
278
  var EXEMPT = ['.claude/', '.claude\\\\', 'CLAUDE.md', 'MEMORY.md', 'workflow-state', 'node_modules', 'moflo.yaml'];
279
- var DANGEROUS = ['rm -rf /', 'format c:', 'del /s /q c:\\\\', ':(){:|:&};:', 'mkfs.', '> /dev/sda'];
279
+ // #1171 DANGEROUS gained PS additions to match the matcher widening that now
280
+ // routes the PowerShell tool through check-dangerous-command. See bin/gate.cjs.
281
+ var DANGEROUS = ['rm -rf /', 'format c:', 'del /s /q c:\\\\', ':(){:|:&};:', 'mkfs.', '> /dev/sda', 'remove-item -recurse -force c:\\\\', 'remove-item -recurse -force /', 'remove-item -recurse -force ~', 'format-volume', 'clear-disk'];
280
282
  // #1132 — Bash memory-first gate regexes. See bin/gate.cjs for documentation.
283
+ // #1171 — READ_LIKE extended with PS-native exploration forms (Get-ChildItem -Recurse,
284
+ // dir /s, Format-Hex). Plain Get-ChildItem stays uncovered (ls-equivalent).
281
285
  var CREDIT_MEMORY_SEARCH_RE = /semantic-search|memory search|memory retrieve|memory-search/;
282
- var READ_LIKE_BASH_RE = /^\\s*(?:cat|head|tail|less|more|bat|xxd|od|hexdump)\\b|^\\s*(?:grep|rg|ag|fgrep|egrep|find|fd)\\b|^\\s*sed\\s+-n\\b|^\\s*awk\\s+(?!.*<<)|^\\s*type\\s+\\S*[\\\\/.]|^\\s*(?:Get-Content|gc|Select-String|sls)\\b/i;
286
+ var READ_LIKE_BASH_RE = /^\\s*(?:cat|head|tail|less|more|bat|xxd|od|hexdump)\\b|^\\s*(?:grep|rg|ag|fgrep|egrep|find|fd)\\b|^\\s*sed\\s+-n\\b|^\\s*awk\\s+(?!.*<<)|^\\s*type\\s+\\S*[\\\\/.]|^\\s*(?:Get-Content|gc|Select-String|sls)\\b|^\\s*(?:Get-ChildItem|gci)\\b[^|]*-Recurse\\b|^\\s*dir\\b[^|]*\\s\\/[sS]\\b|^\\s*Format-Hex\\b/i;
283
287
  var BASH_CARVE_OUT_RE = /^\\s*(npm|npx|pnpm|yarn|bun|node|deno|tsx|ts-node)\\s|^\\s*(git|gh|hub)\\s|^\\s*(docker|kubectl|helm|terraform)\\s|^\\s*(curl|wget|http|fetch)\\s|^\\s*(jq|yq|xq)\\s|^\\s*(echo|printf|true|false|sleep|test|\\[)\\s|^\\s*cat\\s+(<<|<<<)|^\\s*cat\\s+[^|]*\\s*>|^\\s*tee\\b|^\\s*find\\s+.+?-(delete|exec\\s+rm)\\b/;
288
+ // #1171 follow-up — strip quoted bodies + heredocs before DANGEROUS substring
289
+ // match so git commit messages with dangerous-shaped text in quoted bodies do
290
+ // not trip the gate. See bin/gate.cjs for the full rationale. Command-sub
291
+ // bodies are intentionally not stripped (those execute).
292
+ function stripQuotedAndHeredocs(cmd) {
293
+ var out = cmd;
294
+ out = out.replace(/<<-?\\s*['"]?[\\w-]+['"]?[\\s\\S]*$/, '');
295
+ out = out.replace(/<<<\\s*\\S+/g, '');
296
+ out = out.replace(/'[^']*'/g, "''");
297
+ out = out.replace(/"(?:[^"\\\\]|\\\\.)*"/g, '""');
298
+ return out;
299
+ }
300
+
284
301
  var DIRECTIVE_RE = /^(yes|no|yeah|yep|nope|sure|ok|okay|correct|right|exactly|perfect)\\b/i;
285
302
  var TASK_RE = /\\b(fix|bug|error|implement|add|create|build|write|refactor|debug|test|feature|issue|security|optimi)\\b/i;
286
303
 
@@ -585,7 +602,10 @@ switch (command) {
585
602
  process.exit(2);
586
603
  }
587
604
  case 'check-dangerous-command': {
588
- var cmd = (process.env.TOOL_INPUT_command || '').toLowerCase();
605
+ // #1171 follow-up strip quoted bodies + heredocs before substring match.
606
+ // See bin/gate.cjs for full rationale.
607
+ var raw = process.env.TOOL_INPUT_command || '';
608
+ var cmd = stripQuotedAndHeredocs(raw).toLowerCase();
589
609
  for (var i = 0; i < DANGEROUS.length; i++) {
590
610
  if (cmd.indexOf(DANGEROUS[i]) >= 0) {
591
611
  console.log('[BLOCKED] Dangerous command: ' + DANGEROUS[i]);
@@ -219,7 +219,8 @@ function generateHooks(root, force, answers) {
219
219
  "hooks": [{ "type": "command", "command": gateHook('check-before-read'), "timeout": 3000 }]
220
220
  },
221
221
  {
222
- "matcher": "^Bash$",
222
+ // #1171 — widened to cover the dedicated `PowerShell` tool.
223
+ "matcher": "^(Bash|PowerShell)$",
223
224
  "hooks": [
224
225
  { "type": "command", "command": gateHook('check-dangerous-command'), "timeout": 2000 },
225
226
  { "type": "command", "command": gateHook('check-before-pr'), "timeout": 2000 }
@@ -253,7 +254,8 @@ function generateHooks(root, force, answers) {
253
254
  "hooks": [{ "type": "command", "command": gate('record-task-created'), "timeout": 2000 }]
254
255
  },
255
256
  {
256
- "matcher": "^Bash$",
257
+ // #1171 — widened to cover the dedicated `PowerShell` tool.
258
+ "matcher": "^(Bash|PowerShell)$",
257
259
  "hooks": [
258
260
  { "type": "command", "command": gateHook('check-bash-memory'), "timeout": 2000 },
259
261
  { "type": "command", "command": gateHook('record-test-run'), "timeout": 2000 }
@@ -229,12 +229,16 @@ function generateHooksConfig(config) {
229
229
  hooks: [{ type: 'command', command: gateHookCmd('check-before-read'), timeout: 3000 }],
230
230
  },
231
231
  {
232
- matcher: '^Bash$',
232
+ // #1171 — widened from `^Bash$` to also cover the dedicated `PowerShell`
233
+ // tool that Claude Code exposes on Windows. Without this, PS-tool calls
234
+ // route around every Bash-anchored gate (dangerous-command, pr, memory).
235
+ matcher: '^(Bash|PowerShell)$',
233
236
  hooks: [
234
237
  { type: 'command', command: gateHookCmd('check-dangerous-command'), timeout: 2000 },
235
238
  { type: 'command', command: gateHookCmd('check-before-pr'), timeout: 2000 },
236
239
  // #1132 — moved from PostToolUse so process.exit(2) actually blocks
237
- // read-like Bash that bypasses the Read/Glob/Grep gates via the shell.
240
+ // read-like shell commands that bypass the Read/Glob/Grep gates.
241
+ // Name kept for backwards compat; covers PowerShell readers too.
238
242
  { type: 'command', command: gateHookCmd('check-bash-memory'), timeout: 2000 },
239
243
  ],
240
244
  },
@@ -273,7 +277,9 @@ function generateHooksConfig(config) {
273
277
  hooks: [{ type: 'command', command: gateCmd('record-task-created'), timeout: 2000 }],
274
278
  },
275
279
  {
276
- matcher: '^Bash$',
280
+ // #1171 — widened to cover the `PowerShell` tool so PS-invoked
281
+ // `npm test` / `pytest` etc. credit the testing gate the same as Bash.
282
+ matcher: '^(Bash|PowerShell)$',
277
283
  hooks: [
278
284
  // #1132 — check-bash-memory moved to PreToolUse (above).
279
285
  { type: 'command', command: gateHookCmd('record-test-run'), timeout: 2000 },
@@ -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`);
@@ -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);
@@ -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.*`.