moflo 4.9.13 → 4.9.14
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/helpers/gate.cjs +21 -5
- package/.claude/skills/fl/phases.md +18 -2
- package/.claude/skills/simplify/SKILL.md +35 -48
- package/bin/gate.cjs +21 -5
- package/bin/session-start-launcher.mjs +1 -1
- package/bin/simplify-classify.cjs +211 -0
- package/dist/src/cli/commands/doctor-checks-config.js +246 -0
- package/dist/src/cli/commands/doctor-checks-intelligence.js +197 -0
- package/dist/src/cli/commands/doctor-checks-memory.js +207 -0
- package/dist/src/cli/commands/doctor-checks-platform.js +138 -0
- package/dist/src/cli/commands/doctor-checks-runtime.js +170 -0
- package/dist/src/cli/commands/doctor-fixes.js +165 -0
- package/dist/src/cli/commands/doctor-registry.js +109 -0
- package/dist/src/cli/commands/doctor-render.js +203 -0
- package/dist/src/cli/commands/doctor-types.js +9 -0
- package/dist/src/cli/commands/doctor-version.js +134 -0
- package/dist/src/cli/commands/doctor-zombies.js +201 -0
- package/dist/src/cli/commands/doctor.js +35 -1706
- package/dist/src/cli/init/helpers-generator.js +21 -5
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/scripts/post-install-bootstrap.mjs +1 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration & service-discovery checks for `flo doctor`:
|
|
3
|
+
* config files, statusLine, daemon, MCP servers, moflo.yaml compliance,
|
|
4
|
+
* test directories.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync, statSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import { getDaemonLockHolder } from '../services/daemon-lock.js';
|
|
10
|
+
import { legacyMemoryDbPath, memoryDbCandidatePaths, memoryDbPath, } from '../services/moflo-paths.js';
|
|
11
|
+
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
12
|
+
export async function checkConfigFile() {
|
|
13
|
+
// JSON configs (parse-validated). LEGACY-CONFIG: `.claude-flow.json` and
|
|
14
|
+
// `claude-flow.config.json` filenames are still recognised so consumers
|
|
15
|
+
// upgrading from pre-#699 moflo builds (upstream Ruflo) keep working
|
|
16
|
+
// without manual rename. Drift guard exempts these via LEGACY-CONFIG marker.
|
|
17
|
+
const jsonPaths = [
|
|
18
|
+
'.moflo/config.json',
|
|
19
|
+
'moflo.config.json',
|
|
20
|
+
'claude-flow.config.json', // LEGACY-CONFIG: pre-#699 fallback
|
|
21
|
+
'.claude-flow.json', // LEGACY-CONFIG: pre-#699 fallback
|
|
22
|
+
];
|
|
23
|
+
for (const configPath of jsonPaths) {
|
|
24
|
+
if (existsSync(configPath)) {
|
|
25
|
+
try {
|
|
26
|
+
const content = readFileSync(configPath, 'utf8');
|
|
27
|
+
JSON.parse(content);
|
|
28
|
+
return { name: 'Config File', status: 'pass', message: `Found: ${configPath}` };
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { name: 'Config File', status: 'fail', message: `Invalid JSON: ${configPath}`, fix: 'Fix JSON syntax in config file' };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// YAML configs (existence-checked only — no heavy yaml parser dependency).
|
|
36
|
+
const yamlPaths = [
|
|
37
|
+
'.moflo/config.yaml',
|
|
38
|
+
'.moflo/config.yml',
|
|
39
|
+
'moflo.config.yaml',
|
|
40
|
+
'claude-flow.config.yaml', // LEGACY-CONFIG: pre-#699 fallback
|
|
41
|
+
];
|
|
42
|
+
for (const configPath of yamlPaths) {
|
|
43
|
+
if (existsSync(configPath)) {
|
|
44
|
+
return { name: 'Config File', status: 'pass', message: `Found: ${configPath}` };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { name: 'Config File', status: 'warn', message: 'No config file (using defaults)', fix: 'claude-flow config init' };
|
|
48
|
+
}
|
|
49
|
+
export async function checkStatusLine() {
|
|
50
|
+
const settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
|
51
|
+
if (!existsSync(settingsPath)) {
|
|
52
|
+
return { name: 'Status Line', status: 'warn', message: 'No .claude/settings.json found', fix: 'npx moflo init' };
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
56
|
+
if (settings.statusLine && settings.statusLine.command) {
|
|
57
|
+
if (settings.statusLine.command.includes('statusline.cjs')) {
|
|
58
|
+
return { name: 'Status Line', status: 'pass', message: 'Wired in settings.json' };
|
|
59
|
+
}
|
|
60
|
+
return { name: 'Status Line', status: 'pass', message: 'Custom statusLine configured' };
|
|
61
|
+
}
|
|
62
|
+
return { name: 'Status Line', status: 'fail', message: 'statusLine not configured in settings.json', fix: 'Add statusLine config to .claude/settings.json' };
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return { name: 'Status Line', status: 'fail', message: 'Failed to parse .claude/settings.json', fix: 'Fix JSON syntax in .claude/settings.json' };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Delegates to daemon-lock module for proper PID + command-line verification
|
|
69
|
+
// (avoids Windows PID-recycling false positives).
|
|
70
|
+
export async function checkDaemonStatus() {
|
|
71
|
+
try {
|
|
72
|
+
// Retry up to 5 times with 1s delay — the daemon starts in the background
|
|
73
|
+
// during session-start and may not have acquired its lock file yet.
|
|
74
|
+
const MAX_RETRIES = 5;
|
|
75
|
+
const RETRY_DELAY_MS = 1000;
|
|
76
|
+
let holderPid = null;
|
|
77
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
78
|
+
holderPid = getDaemonLockHolder(process.cwd());
|
|
79
|
+
if (holderPid)
|
|
80
|
+
break;
|
|
81
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
82
|
+
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (holderPid) {
|
|
86
|
+
return { name: 'Daemon Status', status: 'pass', message: `Running (PID: ${holderPid})` };
|
|
87
|
+
}
|
|
88
|
+
// getDaemonLockHolder auto-cleans stale locks, but check for legacy PID file
|
|
89
|
+
const lockFile = '.moflo/daemon.lock';
|
|
90
|
+
if (existsSync(lockFile)) {
|
|
91
|
+
return { name: 'Daemon Status', status: 'warn', message: 'Stale lock file', fix: 'rm .moflo/daemon.lock && claude-flow daemon start' };
|
|
92
|
+
}
|
|
93
|
+
const pidFile = '.moflo/daemon.pid';
|
|
94
|
+
if (existsSync(pidFile)) {
|
|
95
|
+
return { name: 'Daemon Status', status: 'warn', message: 'Legacy PID file found', fix: 'rm .moflo/daemon.pid && claude-flow daemon start' };
|
|
96
|
+
}
|
|
97
|
+
return { name: 'Daemon Status', status: 'warn', message: 'Not running', fix: 'claude-flow daemon start' };
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
return { name: 'Daemon Status', status: 'warn', message: `Unable to check: ${errorDetail(e)}`, fix: 'claude-flow daemon status' };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export async function checkMemoryDatabase() {
|
|
104
|
+
const root = process.cwd();
|
|
105
|
+
const canonical = memoryDbPath(root);
|
|
106
|
+
for (const dbPath of memoryDbCandidatePaths(root)) {
|
|
107
|
+
let stats;
|
|
108
|
+
try {
|
|
109
|
+
stats = statSync(dbPath);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
115
|
+
if (dbPath === canonical) {
|
|
116
|
+
let message = `.moflo/moflo.db (${sizeMB} MB)`;
|
|
117
|
+
// Unfinished migration tail: source still present means the launcher's
|
|
118
|
+
// rename-to-.bak step failed (Windows lock most often). Flag so the user
|
|
119
|
+
// knows to clear the stale source.
|
|
120
|
+
if (existsSync(legacyMemoryDbPath(root))) {
|
|
121
|
+
message += ' — legacy .swarm/memory.db still present (delete it after confirming canonical is healthy)';
|
|
122
|
+
}
|
|
123
|
+
return { name: 'Memory Database', status: 'pass', message };
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
name: 'Memory Database',
|
|
127
|
+
status: 'warn',
|
|
128
|
+
message: `${dbPath} (${sizeMB} MB) — legacy location, will migrate to .moflo/moflo.db on next session start`,
|
|
129
|
+
fix: 'restart claude code session',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return { name: 'Memory Database', status: 'warn', message: 'Not initialized', fix: 'claude-flow memory configure --backend hybrid' };
|
|
133
|
+
}
|
|
134
|
+
export async function checkMcpServers() {
|
|
135
|
+
const mcpConfigPaths = [
|
|
136
|
+
join(os.homedir(), '.claude/claude_desktop_config.json'),
|
|
137
|
+
join(os.homedir(), '.config/claude/mcp.json'),
|
|
138
|
+
'.mcp.json',
|
|
139
|
+
// Windows: Claude Desktop stores config under %APPDATA%\Claude\
|
|
140
|
+
...(process.platform === 'win32' && process.env.APPDATA
|
|
141
|
+
? [join(process.env.APPDATA, 'Claude', 'claude_desktop_config.json')]
|
|
142
|
+
: []),
|
|
143
|
+
];
|
|
144
|
+
for (const configPath of mcpConfigPaths) {
|
|
145
|
+
if (existsSync(configPath)) {
|
|
146
|
+
try {
|
|
147
|
+
const content = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
148
|
+
const servers = content.mcpServers || content.servers || {};
|
|
149
|
+
const count = Object.keys(servers).length;
|
|
150
|
+
const hasClaudeFlow = 'moflo' in servers || 'claude-flow' in servers || 'claude-flow_alpha' in servers || 'ruflo' in servers || 'ruflo_alpha' in servers;
|
|
151
|
+
if (hasClaudeFlow) {
|
|
152
|
+
return { name: 'MCP Servers', status: 'pass', message: `${count} servers (flo configured)` };
|
|
153
|
+
}
|
|
154
|
+
return { name: 'MCP Servers', status: 'warn', message: `${count} servers (flo not found)`, fix: 'claude mcp add ruflo -- npx -y ruflo@latest mcp start' };
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// continue to next path
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { name: 'MCP Servers', status: 'warn', message: 'No MCP config found', fix: 'claude mcp add moflo npx moflo mcp start' };
|
|
162
|
+
}
|
|
163
|
+
// Catches three failure modes (#895):
|
|
164
|
+
// 1. File missing — session-start should have created it; warn user that
|
|
165
|
+
// defaults are invisible/untunable.
|
|
166
|
+
// 2. File empty / unreadable — corrupted by half-write or filesystem error.
|
|
167
|
+
// 3. Top-level sections missing — partial yaml from manual edit or stale
|
|
168
|
+
// copy from a moflo version that didn't ship a section yet.
|
|
169
|
+
//
|
|
170
|
+
// Exported so tests can exercise it end-to-end against a temp project root
|
|
171
|
+
// without mutating process.cwd() (which fights vitest's parallel test runner).
|
|
172
|
+
export async function checkMofloYamlCompliance(cwd = process.cwd()) {
|
|
173
|
+
const yamlPath = join(cwd, 'moflo.yaml');
|
|
174
|
+
// Lazy-import the validator so doctor doesn't pull in fs walks on the
|
|
175
|
+
// happy path of unrelated checks.
|
|
176
|
+
const { validateMofloYaml } = await import('../init/moflo-yaml-template.js');
|
|
177
|
+
const result = validateMofloYaml(yamlPath);
|
|
178
|
+
if (!result.exists) {
|
|
179
|
+
return {
|
|
180
|
+
name: 'moflo.yaml',
|
|
181
|
+
status: 'warn',
|
|
182
|
+
message: 'moflo.yaml not found — defaults are in effect but not visible/tunable',
|
|
183
|
+
fix: 'Restart Claude Code (session-start auto-creates) or run `npx moflo init`',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (result.valid) {
|
|
187
|
+
return { name: 'moflo.yaml', status: 'pass', message: `Compliant (${yamlPath})` };
|
|
188
|
+
}
|
|
189
|
+
const parseIssue = result.issues.find((i) => i.kind !== 'missing-section');
|
|
190
|
+
if (parseIssue) {
|
|
191
|
+
return {
|
|
192
|
+
name: 'moflo.yaml',
|
|
193
|
+
status: 'fail',
|
|
194
|
+
message: `${parseIssue.kind}: ${parseIssue.detail}`,
|
|
195
|
+
fix: 'Inspect/repair moflo.yaml, or `mv moflo.yaml moflo.yaml.bak && npx moflo init`',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
name: 'moflo.yaml',
|
|
200
|
+
status: 'warn',
|
|
201
|
+
message: `Missing sections: ${result.missingSections.join(', ')}`,
|
|
202
|
+
fix: 'Restart Claude Code (yaml-upgrader auto-appends) or `npx moflo init --force`',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
export async function checkTestDirs() {
|
|
206
|
+
const yamlPath = join(process.cwd(), 'moflo.yaml');
|
|
207
|
+
if (!existsSync(yamlPath)) {
|
|
208
|
+
return { name: 'Test Directories', status: 'warn', message: 'No moflo.yaml — test indexing unconfigured', fix: 'npx moflo init' };
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
const content = readFileSync(yamlPath, 'utf-8');
|
|
212
|
+
const testsBlock = content.match(/tests:\s*\n\s+directories:\s*\n((?:\s+-\s+.+\n?)+)/);
|
|
213
|
+
if (!testsBlock) {
|
|
214
|
+
return { name: 'Test Directories', status: 'warn', message: 'No tests section in moflo.yaml', fix: 'npx moflo init --force' };
|
|
215
|
+
}
|
|
216
|
+
const items = testsBlock[1].match(/-\s+(.+)/g);
|
|
217
|
+
if (!items || items.length === 0) {
|
|
218
|
+
return { name: 'Test Directories', status: 'warn', message: 'Empty test directories list' };
|
|
219
|
+
}
|
|
220
|
+
const dirs = items.map(item => item.replace(/^-\s+/, '').trim());
|
|
221
|
+
const existing = dirs.filter(d => existsSync(join(process.cwd(), d)));
|
|
222
|
+
const missing = dirs.filter(d => !existsSync(join(process.cwd(), d)));
|
|
223
|
+
const autoIndexMatch = content.match(/auto_index:\s*\n(?:.*\n)*?\s+tests:\s*(true|false)/);
|
|
224
|
+
const autoIndexEnabled = !autoIndexMatch || autoIndexMatch[1] !== 'false';
|
|
225
|
+
const indexLabel = autoIndexEnabled ? 'auto-index: on' : 'auto-index: off';
|
|
226
|
+
if (missing.length > 0 && existing.length === 0) {
|
|
227
|
+
return {
|
|
228
|
+
name: 'Test Directories',
|
|
229
|
+
status: 'warn',
|
|
230
|
+
message: `No configured test dirs exist: ${missing.join(', ')} (${indexLabel})`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (missing.length > 0) {
|
|
234
|
+
return {
|
|
235
|
+
name: 'Test Directories',
|
|
236
|
+
status: 'warn',
|
|
237
|
+
message: `${existing.length} OK, ${missing.length} missing: ${missing.join(', ')} (${indexLabel})`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return { name: 'Test Directories', status: 'pass', message: `${existing.length} directories: ${existing.join(', ')} (${indexLabel})` };
|
|
241
|
+
}
|
|
242
|
+
catch (e) {
|
|
243
|
+
return { name: 'Test Directories', status: 'warn', message: `Unable to parse moflo.yaml: ${errorDetail(e)}` };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=doctor-checks-config.js.map
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intelligence-layer functional checks for `flo doctor`:
|
|
3
|
+
* SONA, ReasoningBank, PatternLearner, MicroLoRA + EWC++, RL algorithms.
|
|
4
|
+
*
|
|
5
|
+
* Each component is exercised with a lightweight functional test rather than
|
|
6
|
+
* just checking "loaded".
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
// Memory-backed pattern fallback (populated by pretrain) when an in-process
|
|
11
|
+
// neural component returns no data. Uses the pattern-search handler that
|
|
12
|
+
// pretrain writes to.
|
|
13
|
+
async function checkMemoryPatterns(_namespace) {
|
|
14
|
+
try {
|
|
15
|
+
const hooksMod = await import('../mcp-tools/hooks-tools.js');
|
|
16
|
+
if (hooksMod.hooksPatternSearch) {
|
|
17
|
+
const result = await hooksMod.hooksPatternSearch.handler({
|
|
18
|
+
query: 'pretrain',
|
|
19
|
+
topK: 1,
|
|
20
|
+
minConfidence: 0.1,
|
|
21
|
+
});
|
|
22
|
+
const matches = result?.results;
|
|
23
|
+
if (Array.isArray(matches))
|
|
24
|
+
return matches.length;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// hooks module not available
|
|
29
|
+
}
|
|
30
|
+
// Secondary fallback: check the memory DB file exists
|
|
31
|
+
const dbPath = join(process.cwd(), '.claude', 'memory.db');
|
|
32
|
+
if (existsSync(dbPath))
|
|
33
|
+
return 1;
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
export async function checkIntelligence() {
|
|
37
|
+
try {
|
|
38
|
+
const neural = await import('../neural/index.js');
|
|
39
|
+
const results = [];
|
|
40
|
+
const failures = [];
|
|
41
|
+
// 1. SONA — create manager, run trajectory lifecycle
|
|
42
|
+
try {
|
|
43
|
+
const sona = neural.createSONAManager('balanced');
|
|
44
|
+
await sona.initialize();
|
|
45
|
+
const tid = sona.beginTrajectory('doctor-check', 'general');
|
|
46
|
+
const embedding = new Float32Array(64).fill(0.1);
|
|
47
|
+
sona.recordStep(tid, 'test-action', 0.8, embedding);
|
|
48
|
+
const traj = sona.completeTrajectory(tid, 0.9);
|
|
49
|
+
if (traj && traj.steps.length > 0) {
|
|
50
|
+
results.push('SONA');
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
failures.push('SONA (no trajectory output)');
|
|
54
|
+
}
|
|
55
|
+
await sona.cleanup();
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
failures.push(`SONA (${e instanceof Error ? e.message : 'error'})`);
|
|
59
|
+
}
|
|
60
|
+
// 2. ReasoningBank — verify instantiation and trajectory store/distill lifecycle
|
|
61
|
+
try {
|
|
62
|
+
const rb = neural.createReasoningBank();
|
|
63
|
+
const stateAfter = new Float32Array(64).fill(0.2);
|
|
64
|
+
const trajectory = {
|
|
65
|
+
trajectoryId: 'doctor-test',
|
|
66
|
+
context: 'health check',
|
|
67
|
+
domain: 'general',
|
|
68
|
+
steps: [{ stepId: 's1', action: 'test', reward: 1, stateBefore: stateAfter, stateAfter, timestamp: Date.now() }],
|
|
69
|
+
startTime: Date.now(),
|
|
70
|
+
endTime: Date.now(),
|
|
71
|
+
qualityScore: 0.9,
|
|
72
|
+
isComplete: true,
|
|
73
|
+
verdict: {
|
|
74
|
+
success: true,
|
|
75
|
+
confidence: 0.9,
|
|
76
|
+
strengths: ['health check passed'],
|
|
77
|
+
weaknesses: [],
|
|
78
|
+
improvements: [],
|
|
79
|
+
relevanceScore: 0.9,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
rb.storeTrajectory(trajectory);
|
|
83
|
+
// distill() populates memories (storeTrajectory alone does not)
|
|
84
|
+
const distilled = await rb.distill(trajectory);
|
|
85
|
+
if (distilled || rb.getTrajectories().length > 0) {
|
|
86
|
+
results.push('ReasoningBank');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const memoryPatterns = await checkMemoryPatterns('patterns');
|
|
90
|
+
if (memoryPatterns > 0) {
|
|
91
|
+
results.push('ReasoningBank(memory)');
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
failures.push('ReasoningBank (distill returned no data)');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
failures.push(`ReasoningBank (${e instanceof Error ? e.message : 'error'})`);
|
|
100
|
+
}
|
|
101
|
+
// 3. PatternLearner — extract + match
|
|
102
|
+
try {
|
|
103
|
+
const pl = neural.createPatternLearner();
|
|
104
|
+
const embedding = new Float32Array(64).fill(0.3);
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
pl.extractPattern({
|
|
107
|
+
trajectoryId: 'doctor-pl', context: 'test', domain: 'general',
|
|
108
|
+
steps: [{ stepId: 's1', action: 'test', reward: 1, stateBefore: embedding, stateAfter: embedding, timestamp: now }],
|
|
109
|
+
startTime: now, endTime: now, qualityScore: 1, isComplete: true,
|
|
110
|
+
}, { memoryId: 'doctor-pl-mem', trajectoryId: 'doctor-pl', strategy: 'health-check', keyLearnings: ['test'], embedding, quality: 1, usageCount: 0, lastUsed: now });
|
|
111
|
+
const matches = pl.findMatches(embedding, 1);
|
|
112
|
+
if (matches.length > 0) {
|
|
113
|
+
results.push('PatternLearner');
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const memoryPatterns = await checkMemoryPatterns('patterns');
|
|
117
|
+
if (memoryPatterns > 0) {
|
|
118
|
+
results.push('PatternLearner(memory)');
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
failures.push('PatternLearner (no matches)');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
failures.push(`PatternLearner (${e instanceof Error ? e.message : 'error'})`);
|
|
127
|
+
}
|
|
128
|
+
// 4. SONALearningEngine (MicroLoRA + EWC++)
|
|
129
|
+
try {
|
|
130
|
+
const engine = neural.createSONALearningEngine();
|
|
131
|
+
const ctx = { domain: 'general', queryEmbedding: new Float32Array(768).fill(0.1) };
|
|
132
|
+
const adapted = await engine.adapt(ctx);
|
|
133
|
+
const components = [];
|
|
134
|
+
if (adapted && adapted.transformedQuery)
|
|
135
|
+
components.push('LoRA');
|
|
136
|
+
if (adapted && adapted.patterns !== undefined)
|
|
137
|
+
components.push('EWC++');
|
|
138
|
+
if (components.length > 0) {
|
|
139
|
+
results.push(...components);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
failures.push('LoRA/EWC++ (adapt returned no data)');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
// Gracefully handle cold/uninitialized state
|
|
147
|
+
const msg = e instanceof Error ? e.message : 'error';
|
|
148
|
+
if (msg.includes('undefined') || msg.includes('not initialized')) {
|
|
149
|
+
results.push('LoRA/EWC++(cold)');
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
failures.push(`LoRA/EWC++ (${msg})`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// 5. RL Algorithms — quick instantiation check
|
|
156
|
+
try {
|
|
157
|
+
const algNames = [];
|
|
158
|
+
const ppo = neural.createPPO();
|
|
159
|
+
if (ppo)
|
|
160
|
+
algNames.push('PPO');
|
|
161
|
+
const dqn = neural.createDQN();
|
|
162
|
+
if (dqn)
|
|
163
|
+
algNames.push('DQN');
|
|
164
|
+
const ql = neural.createQLearning();
|
|
165
|
+
if (ql)
|
|
166
|
+
algNames.push('Q-Learn');
|
|
167
|
+
if (algNames.length > 0) {
|
|
168
|
+
results.push(`RL(${algNames.join('+')})`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
failures.push(`RL (${e instanceof Error ? e.message : 'error'})`);
|
|
173
|
+
}
|
|
174
|
+
if (failures.length > 0) {
|
|
175
|
+
return {
|
|
176
|
+
name: 'Intelligence',
|
|
177
|
+
status: results.length > 0 ? 'warn' : 'fail',
|
|
178
|
+
message: `${results.join(', ')} OK; FAILED: ${failures.join(', ')}`,
|
|
179
|
+
fix: 'Check neural module imports and dependencies',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
name: 'Intelligence',
|
|
184
|
+
status: 'pass',
|
|
185
|
+
message: results.join(', '),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
return {
|
|
190
|
+
name: 'Intelligence',
|
|
191
|
+
status: 'warn',
|
|
192
|
+
message: `Module unavailable: ${e instanceof Error ? e.message.split(/\r?\n/)[0] : 'import failed'}`,
|
|
193
|
+
fix: 'Ensure moflo is built (npm run build)',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=doctor-checks-intelligence.js.map
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory + embeddings + semantic-search checks for `flo doctor`.
|
|
3
|
+
*
|
|
4
|
+
* `checkEmbeddings` is exported for the #639 stale-cache regression test
|
|
5
|
+
* (src/cli/__tests__/commands/doctor-stale-vector-stats.test.ts).
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync, statSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { memoryDbCandidatePaths } from '../services/moflo-paths.js';
|
|
10
|
+
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
11
|
+
/** Skew (cached / live count delta) above which the cache is treated as stale. */
|
|
12
|
+
const VECTOR_STATS_SKEW_WARN_THRESHOLD = 0.2;
|
|
13
|
+
/**
|
|
14
|
+
* Open `dbPath` via moflo's bundled sql.js and return the count of memory_entries
|
|
15
|
+
* rows that have an embedding. Returns null if sql.js can't be loaded, the file
|
|
16
|
+
* isn't a v3 schema, or the query fails — every error is treated as "unknown
|
|
17
|
+
* truth", letting the caller fall back to the cached stats rather than masking
|
|
18
|
+
* a healthy DB as broken.
|
|
19
|
+
*/
|
|
20
|
+
async function countEmbeddedRowsFromDb(dbPath) {
|
|
21
|
+
try {
|
|
22
|
+
const { mofloImport } = await import('../services/moflo-require.js');
|
|
23
|
+
const initSqlJs = (await mofloImport('sql.js'))?.default;
|
|
24
|
+
if (!initSqlJs)
|
|
25
|
+
return null;
|
|
26
|
+
const SQL = await initSqlJs();
|
|
27
|
+
const buffer = readFileSync(dbPath);
|
|
28
|
+
const db = new SQL.Database(buffer);
|
|
29
|
+
try {
|
|
30
|
+
const res = db.exec("SELECT COUNT(*) FROM memory_entries WHERE embedding IS NOT NULL AND embedding != ''");
|
|
31
|
+
const cell = res?.[0]?.values?.[0]?.[0];
|
|
32
|
+
return typeof cell === 'number' ? cell : Number(cell ?? 0);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
db.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function checkEmbeddings() {
|
|
43
|
+
const liveDbPath = memoryDbCandidatePaths(process.cwd()).find((p) => existsSync(p));
|
|
44
|
+
// 1. Fast path: read cached vector-stats.json if available
|
|
45
|
+
const statsPath = join(process.cwd(), '.moflo', 'vector-stats.json');
|
|
46
|
+
try {
|
|
47
|
+
if (existsSync(statsPath)) {
|
|
48
|
+
const stats = JSON.parse(readFileSync(statsPath, 'utf8'));
|
|
49
|
+
const count = stats.vectorCount ?? 0;
|
|
50
|
+
const updatedAt = typeof stats.updatedAt === 'number' ? stats.updatedAt : 0;
|
|
51
|
+
const hasHnsw = stats.hasHnsw ?? false;
|
|
52
|
+
const dbSizeKB = stats.dbSizeKB ?? 0;
|
|
53
|
+
// Skew check (#639): cross-check the cached vectorCount against the actual
|
|
54
|
+
// DB; if they differ by more than VECTOR_STATS_SKEW_WARN_THRESHOLD, surface
|
|
55
|
+
// a stale-cache warning rather than displaying a wrong number on the
|
|
56
|
+
// statusline. Cheap signals first — opening memory.db via sql.js loads the
|
|
57
|
+
// whole file. Skip the open when the cache was clearly written after the
|
|
58
|
+
// last DB mutation (mtime check) AND the cached count is non-zero. The
|
|
59
|
+
// count===0 case keeps the open because that's the observed #639 failure
|
|
60
|
+
// mode (cache silently clobbered to zero).
|
|
61
|
+
let dbMtimeMs = 0;
|
|
62
|
+
if (liveDbPath) {
|
|
63
|
+
try {
|
|
64
|
+
dbMtimeMs = statSync(liveDbPath).mtimeMs;
|
|
65
|
+
}
|
|
66
|
+
catch { /* missing — handled below */ }
|
|
67
|
+
}
|
|
68
|
+
const cacheNewerThanDb = updatedAt > 0 && dbMtimeMs > 0 && updatedAt >= dbMtimeMs;
|
|
69
|
+
if (liveDbPath && (count === 0 || !cacheNewerThanDb)) {
|
|
70
|
+
const liveCount = await countEmbeddedRowsFromDb(liveDbPath);
|
|
71
|
+
if (liveCount !== null) {
|
|
72
|
+
const denom = Math.max(liveCount, 1);
|
|
73
|
+
const skew = Math.abs(liveCount - count) / denom;
|
|
74
|
+
if (skew > VECTOR_STATS_SKEW_WARN_THRESHOLD) {
|
|
75
|
+
return {
|
|
76
|
+
name: 'Embeddings',
|
|
77
|
+
status: 'warn',
|
|
78
|
+
message: `vector-stats cache is stale (cached ${count}, DB has ${liveCount} embedded rows — ${Math.round(skew * 100)}% skew)`,
|
|
79
|
+
fix: 'node node_modules/moflo/bin/build-embeddings.mjs',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (count === 0) {
|
|
85
|
+
return {
|
|
86
|
+
name: 'Embeddings',
|
|
87
|
+
status: 'warn',
|
|
88
|
+
message: `Memory DB exists (${dbSizeKB} KB) but 0 vectors indexed — documents not embedded`,
|
|
89
|
+
fix: 'npx moflo memory init --force && npx moflo embeddings init',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const hnswLabel = hasHnsw ? ', HNSW' : '';
|
|
93
|
+
return {
|
|
94
|
+
name: 'Embeddings',
|
|
95
|
+
status: 'pass',
|
|
96
|
+
message: `${count} vectors indexed (${dbSizeKB} KB${hnswLabel})`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Stats file unreadable — fall through to DB check
|
|
102
|
+
}
|
|
103
|
+
// 2. Check if memory DB file exists at all (reuse liveDbPath from above)
|
|
104
|
+
const foundDbPath = liveDbPath ?? null;
|
|
105
|
+
if (!foundDbPath) {
|
|
106
|
+
return {
|
|
107
|
+
name: 'Embeddings',
|
|
108
|
+
status: 'warn',
|
|
109
|
+
message: 'No memory database — embeddings not initialized',
|
|
110
|
+
fix: 'npx moflo memory init --force',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// 3. DB exists but no stats cache — try querying the DB for entry count
|
|
114
|
+
try {
|
|
115
|
+
const { checkMemoryInitialization } = await import('../memory/memory-initializer.js');
|
|
116
|
+
const info = await checkMemoryInitialization(foundDbPath);
|
|
117
|
+
if (!info.initialized) {
|
|
118
|
+
return {
|
|
119
|
+
name: 'Embeddings',
|
|
120
|
+
status: 'warn',
|
|
121
|
+
message: 'Memory DB exists but not properly initialized',
|
|
122
|
+
fix: 'npx moflo memory init --force',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const hasVectors = info.features?.vectorEmbeddings ?? false;
|
|
126
|
+
if (!hasVectors) {
|
|
127
|
+
return {
|
|
128
|
+
name: 'Embeddings',
|
|
129
|
+
status: 'warn',
|
|
130
|
+
message: `Memory DB initialized (v${info.version}) but no vector_indexes table`,
|
|
131
|
+
fix: 'npx moflo memory init --force && npx moflo embeddings init',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
name: 'Embeddings',
|
|
136
|
+
status: 'pass',
|
|
137
|
+
message: `Memory DB initialized (v${info.version}, vectors enabled)`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch (sqlJsError) {
|
|
141
|
+
// sql.js not available — fall back to file-size heuristic
|
|
142
|
+
const sqlDetail = errorDetail(sqlJsError);
|
|
143
|
+
try {
|
|
144
|
+
const stats = statSync(foundDbPath);
|
|
145
|
+
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
146
|
+
return {
|
|
147
|
+
name: 'Embeddings',
|
|
148
|
+
status: 'warn',
|
|
149
|
+
message: `Memory DB exists (${sizeMB} MB) — cannot verify vectors (sql.js not available: ${sqlDetail})`,
|
|
150
|
+
fix: 'npm install sql.js && npx moflo embeddings init',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch (statError) {
|
|
154
|
+
return { name: 'Embeddings', status: 'warn', message: `Unable to check: sql.js failed (${sqlDetail}), stat failed (${errorDetail(statError)})` };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export async function checkSemanticQuality() {
|
|
159
|
+
try {
|
|
160
|
+
const { searchEntries } = await import('../memory/memory-initializer.js');
|
|
161
|
+
const result = await searchEntries({
|
|
162
|
+
query: 'test infrastructure health check',
|
|
163
|
+
namespace: 'patterns',
|
|
164
|
+
limit: 5,
|
|
165
|
+
threshold: 0.1,
|
|
166
|
+
});
|
|
167
|
+
if (!result.success || result.results.length === 0) {
|
|
168
|
+
return {
|
|
169
|
+
name: 'Semantic Quality',
|
|
170
|
+
status: 'warn',
|
|
171
|
+
message: 'No search results (empty database or no patterns namespace)',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const scores = result.results.map((r) => r.score);
|
|
175
|
+
const allSame = scores.every((s) => s === scores[0]);
|
|
176
|
+
const hasFallback = scores.some((s) => s === 0.5);
|
|
177
|
+
if (hasFallback) {
|
|
178
|
+
return {
|
|
179
|
+
name: 'Semantic Quality',
|
|
180
|
+
status: 'fail',
|
|
181
|
+
message: `${scores.length} results, scores include 0.500 fallback (keyword-only, no embeddings)`,
|
|
182
|
+
fix: 'Re-index with: npx moflo embeddings build --force',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (allSame && scores.length > 1) {
|
|
186
|
+
return {
|
|
187
|
+
name: 'Semantic Quality',
|
|
188
|
+
status: 'warn',
|
|
189
|
+
message: `${scores.length} results, all scores identical (${scores[0].toFixed(3)}) — degraded search`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const topScore = Math.max(...scores);
|
|
193
|
+
return {
|
|
194
|
+
name: 'Semantic Quality',
|
|
195
|
+
status: topScore >= 0.3 ? 'pass' : 'warn',
|
|
196
|
+
message: `${scores.length} results, top ${topScore.toFixed(3)}, varied (semantic search active)`,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
return {
|
|
201
|
+
name: 'Semantic Quality',
|
|
202
|
+
status: 'warn',
|
|
203
|
+
message: `Check failed: ${e instanceof Error ? e.message.split(/\r?\n/)[0] : 'error'}`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=doctor-checks-memory.js.map
|