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.
- package/.claude/guidance/shipped/moflo-core-guidance.md +16 -0
- package/.claude/guidance/shipped/moflo-memory-protocol.md +171 -11
- package/.claude/helpers/gate.cjs +139 -14
- package/.claude/skills/publish/SKILL.md +46 -8
- package/bin/gate.cjs +139 -14
- package/bin/lib/moflo-paths.mjs +74 -4
- package/bin/session-start-launcher.mjs +173 -5
- package/dist/src/cli/commands/doctor-checks-config.js +141 -3
- package/dist/src/cli/commands/doctor-fixes.js +202 -10
- package/dist/src/cli/commands/doctor-registry.js +9 -1
- package/dist/src/cli/commands/init.js +33 -0
- package/dist/src/cli/commands/memory.js +11 -4
- package/dist/src/cli/commands/swarm.js +29 -60
- package/dist/src/cli/init/claudemd-generator.js +6 -2
- package/dist/src/cli/init/helpers-generator.js +23 -3
- package/dist/src/cli/init/moflo-init.js +4 -2
- package/dist/src/cli/init/settings-generator.js +9 -3
- package/dist/src/cli/mcp-server.js +104 -2
- package/dist/src/cli/memory/ewc-consolidation.js +22 -6
- package/dist/src/cli/memory/sona-optimizer.js +25 -7
- package/dist/src/cli/movector/lora-adapter.js +22 -7
- package/dist/src/cli/movector/moe-router.js +22 -6
- package/dist/src/cli/services/hook-block-hash.js +5 -2
- package/dist/src/cli/services/hook-wiring.js +38 -4
- package/dist/src/cli/services/moflo-paths.js +36 -0
- package/dist/src/cli/services/project-root.js +84 -25
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 .
|
|
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
|
-
|
|
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
|
-
|
|
451
|
-
|
|
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(
|
|
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 .
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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(
|
|
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 .
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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.*`.
|