moflo 4.9.12 → 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/eldar/SKILL.md +305 -0
- package/.claude/skills/fl/phases.md +18 -2
- package/.claude/skills/simplify/SKILL.md +35 -48
- package/README.md +25 -0
- package/bin/gate.cjs +21 -5
- package/bin/hooks.mjs +2 -2
- package/bin/index-guidance.mjs +14 -24
- package/bin/index-patterns.mjs +13 -10
- package/bin/session-start-launcher.mjs +64 -10
- 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-deep.js +14 -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 -1657
- package/dist/src/cli/init/helpers-generator.js +21 -5
- package/dist/src/cli/init/moflo-init.js +20 -268
- package/dist/src/cli/init/moflo-yaml-template.js +370 -0
- package/dist/src/cli/mcp-tools/hooks-tools.js +3 -1
- package/dist/src/cli/movector/model-router.js +66 -20
- package/dist/src/cli/services/hook-block-hash.js +23 -2
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/scripts/post-install-bootstrap.mjs +1 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick runtime environment checks for `flo doctor`:
|
|
3
|
+
* Node.js, npm, Claude Code CLI, git, disk space, TypeScript build tool.
|
|
4
|
+
*
|
|
5
|
+
* These checks call external binaries via the shared `runCommand` helper that
|
|
6
|
+
* inherits the full process env (critical on Windows where PATH may not be
|
|
7
|
+
* inherited properly across child shells).
|
|
8
|
+
*/
|
|
9
|
+
import { execSync, exec } from 'child_process';
|
|
10
|
+
import { promisify } from 'util';
|
|
11
|
+
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
12
|
+
import { output } from '../output.js';
|
|
13
|
+
const execAsync = promisify(exec);
|
|
14
|
+
/**
|
|
15
|
+
* Execute command asynchronously with proper environment inheritance.
|
|
16
|
+
* Critical for Windows where PATH may not be inherited properly.
|
|
17
|
+
*/
|
|
18
|
+
export async function runCommand(command, timeoutMs = 5000) {
|
|
19
|
+
const opts = {
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
timeout: timeoutMs,
|
|
22
|
+
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/sh',
|
|
23
|
+
env: { ...process.env },
|
|
24
|
+
windowsHide: true,
|
|
25
|
+
};
|
|
26
|
+
const { stdout } = await execAsync(command, opts);
|
|
27
|
+
const out = stdout.trim();
|
|
28
|
+
// Windows parallel exec occasionally returns empty stdout under shell contention — retry once serially
|
|
29
|
+
if (!out && process.platform === 'win32') {
|
|
30
|
+
const retry = await execAsync(command, opts);
|
|
31
|
+
return retry.stdout.trim();
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
export async function checkNodeVersion() {
|
|
36
|
+
const requiredMajor = 20;
|
|
37
|
+
const version = process.version;
|
|
38
|
+
const major = parseInt(version.slice(1).split('.')[0], 10);
|
|
39
|
+
if (major >= requiredMajor) {
|
|
40
|
+
return { name: 'Node.js Version', status: 'pass', message: `${version} (>= ${requiredMajor} required)` };
|
|
41
|
+
}
|
|
42
|
+
else if (major >= 18) {
|
|
43
|
+
return { name: 'Node.js Version', status: 'warn', message: `${version} (>= ${requiredMajor} recommended)`, fix: 'nvm install 20 && nvm use 20' };
|
|
44
|
+
}
|
|
45
|
+
return { name: 'Node.js Version', status: 'fail', message: `${version} (>= ${requiredMajor} required)`, fix: 'nvm install 20 && nvm use 20' };
|
|
46
|
+
}
|
|
47
|
+
export async function checkNpmVersion() {
|
|
48
|
+
try {
|
|
49
|
+
const version = await runCommand('npm --version');
|
|
50
|
+
const major = parseInt(version.split('.')[0], 10);
|
|
51
|
+
if (major >= 9) {
|
|
52
|
+
return { name: 'npm Version', status: 'pass', message: `v${version}` };
|
|
53
|
+
}
|
|
54
|
+
return { name: 'npm Version', status: 'warn', message: `v${version} (>= 9 recommended)`, fix: 'npm install -g npm@latest' };
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return { name: 'npm Version', status: 'fail', message: 'npm not found', fix: 'Install Node.js from https://nodejs.org' };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function checkGit() {
|
|
61
|
+
try {
|
|
62
|
+
const version = await runCommand('git --version');
|
|
63
|
+
return { name: 'Git', status: 'pass', message: version.replace('git version ', 'v') };
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
return { name: 'Git', status: 'warn', message: `Not installed (${errorDetail(e, { firstLineOnly: true })})`, fix: 'Install git from https://git-scm.com' };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function checkGitRepo() {
|
|
70
|
+
try {
|
|
71
|
+
await runCommand('git rev-parse --git-dir');
|
|
72
|
+
return { name: 'Git Repository', status: 'pass', message: 'In a git repository' };
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
return { name: 'Git Repository', status: 'warn', message: `Not a git repository (${errorDetail(e, { firstLineOnly: true })})`, fix: 'git init' };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function checkDiskSpace() {
|
|
79
|
+
try {
|
|
80
|
+
if (process.platform === 'win32') {
|
|
81
|
+
try {
|
|
82
|
+
const driveLetter = process.cwd().match(/^([A-Z]):/i)?.[1]?.toUpperCase() || 'C';
|
|
83
|
+
const psOutput = await runCommand(`powershell -NoProfile -Command "Get-PSDrive ${driveLetter} | Select-Object -ExpandProperty Free; Get-PSDrive ${driveLetter} | Select-Object -ExpandProperty Used"`);
|
|
84
|
+
const vals = psOutput.split(/\r?\n/).filter(l => l.trim());
|
|
85
|
+
const freeBytes = parseInt(vals[0] || '0', 10);
|
|
86
|
+
const usedBytes = parseInt(vals[1] || '0', 10);
|
|
87
|
+
const totalBytes = freeBytes + usedBytes || 1;
|
|
88
|
+
const freeGB = (freeBytes / (1024 ** 3)).toFixed(1);
|
|
89
|
+
const usePercent = Math.round(((totalBytes - freeBytes) / totalBytes) * 100);
|
|
90
|
+
if (usePercent > 90) {
|
|
91
|
+
return { name: 'Disk Space', status: 'fail', message: `${freeGB}G available (${usePercent}% used)`, fix: 'Free up disk space' };
|
|
92
|
+
}
|
|
93
|
+
if (usePercent > 80) {
|
|
94
|
+
return { name: 'Disk Space', status: 'warn', message: `${freeGB}G available (${usePercent}% used)` };
|
|
95
|
+
}
|
|
96
|
+
return { name: 'Disk Space', status: 'pass', message: `${freeGB}G available` };
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return { name: 'Disk Space', status: 'pass', message: 'Check skipped (PowerShell unavailable)' };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Use df -Ph for POSIX mode (guarantees single-line output even with long device names)
|
|
103
|
+
const output_str = await runCommand('df -Ph . | tail -1');
|
|
104
|
+
const parts = output_str.split(/\s+/);
|
|
105
|
+
const available = parts[3];
|
|
106
|
+
const usePercent = parseInt(parts[4]?.replace('%', '') || '0', 10);
|
|
107
|
+
if (isNaN(usePercent)) {
|
|
108
|
+
return { name: 'Disk Space', status: 'warn', message: `${available || 'unknown'} available (unable to parse usage)` };
|
|
109
|
+
}
|
|
110
|
+
if (usePercent > 90) {
|
|
111
|
+
return { name: 'Disk Space', status: 'fail', message: `${available} available (${usePercent}% used)`, fix: 'Free up disk space' };
|
|
112
|
+
}
|
|
113
|
+
if (usePercent > 80) {
|
|
114
|
+
return { name: 'Disk Space', status: 'warn', message: `${available} available (${usePercent}% used)` };
|
|
115
|
+
}
|
|
116
|
+
return { name: 'Disk Space', status: 'pass', message: `${available} available` };
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
return { name: 'Disk Space', status: 'warn', message: `Unable to check: ${errorDetail(e, { firstLineOnly: true })}` };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export async function checkBuildTools() {
|
|
123
|
+
try {
|
|
124
|
+
const tscVersion = await runCommand('npx tsc --version', 10000); // tsc can be slow
|
|
125
|
+
if (!tscVersion || tscVersion.includes('not found')) {
|
|
126
|
+
return { name: 'TypeScript', status: 'warn', message: 'Not installed locally', fix: 'npm install -D typescript' };
|
|
127
|
+
}
|
|
128
|
+
return { name: 'TypeScript', status: 'pass', message: tscVersion.replace('Version ', 'v') };
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
return { name: 'TypeScript', status: 'warn', message: `Not installed locally (${errorDetail(e, { firstLineOnly: true })})`, fix: 'npm install -D typescript' };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export async function checkClaudeCode() {
|
|
135
|
+
try {
|
|
136
|
+
const version = await runCommand('claude --version');
|
|
137
|
+
const versionMatch = version.match(/v?(\d+\.\d+\.\d+)/);
|
|
138
|
+
const versionStr = versionMatch ? `v${versionMatch[1]}` : version;
|
|
139
|
+
return { name: 'Claude Code CLI', status: 'pass', message: versionStr };
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
return {
|
|
143
|
+
name: 'Claude Code CLI',
|
|
144
|
+
status: 'warn',
|
|
145
|
+
message: `Not installed (${errorDetail(e, { firstLineOnly: true })})`,
|
|
146
|
+
fix: 'npm install -g @anthropic-ai/claude-code',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export async function installClaudeCode() {
|
|
151
|
+
try {
|
|
152
|
+
output.writeln();
|
|
153
|
+
output.writeln(output.bold('Installing Claude Code CLI...'));
|
|
154
|
+
execSync('npm install -g @anthropic-ai/claude-code', {
|
|
155
|
+
encoding: 'utf8',
|
|
156
|
+
stdio: 'inherit',
|
|
157
|
+
windowsHide: true,
|
|
158
|
+
});
|
|
159
|
+
output.writeln(output.success('Claude Code CLI installed successfully!'));
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
output.writeln(output.error('Failed to install Claude Code CLI'));
|
|
164
|
+
if (error instanceof Error) {
|
|
165
|
+
output.writeln(output.dim(error.message));
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=doctor-checks-runtime.js.map
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-fix dispatch for `flo doctor --fix`.
|
|
3
|
+
*
|
|
4
|
+
* Maps each named HealthCheck to a programmatic fix function (preferred over
|
|
5
|
+
* shell-out where possible). Falls back to running the check's `fix` string
|
|
6
|
+
* if it looks like an `npx`/`npm`/`claude` command.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { output } from '../output.js';
|
|
11
|
+
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
12
|
+
import { repairHookWiring } from '../services/hook-wiring.js';
|
|
13
|
+
import { findZombieProcesses } from './doctor-zombies.js';
|
|
14
|
+
import { installClaudeCode, runCommand } from './doctor-checks-runtime.js';
|
|
15
|
+
/** Run a shell command as a fix action. Returns true on exit code 0. */
|
|
16
|
+
async function runFixCommand(cmd) {
|
|
17
|
+
try {
|
|
18
|
+
await runCommand(cmd, 30_000);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Fix missing hook wiring in settings.json by patching in entries for any
|
|
27
|
+
* REQUIRED_HOOK_WIRING patterns that aren't present. Delegates to shared
|
|
28
|
+
* repairHookWiring() to stay DRY with the upgrade path.
|
|
29
|
+
*/
|
|
30
|
+
async function fixGateHealthHooks() {
|
|
31
|
+
const settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
|
32
|
+
if (!existsSync(settingsPath))
|
|
33
|
+
return false;
|
|
34
|
+
try {
|
|
35
|
+
const raw = readFileSync(settingsPath, 'utf8');
|
|
36
|
+
const settings = JSON.parse(raw);
|
|
37
|
+
const { repaired } = repairHookWiring(settings);
|
|
38
|
+
if (repaired.length === 0)
|
|
39
|
+
return true; // nothing to fix
|
|
40
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Execute the fix for a failed/warned health check.
|
|
49
|
+
* Returns true if the fix succeeded (re-check should pass).
|
|
50
|
+
*/
|
|
51
|
+
export async function autoFixCheck(check) {
|
|
52
|
+
if (!check.fix)
|
|
53
|
+
return false;
|
|
54
|
+
// Map checks to programmatic fixes (not just shell commands)
|
|
55
|
+
const fixActions = {
|
|
56
|
+
'Memory Database': async () => {
|
|
57
|
+
try {
|
|
58
|
+
const swarmDir = join(process.cwd(), '.swarm');
|
|
59
|
+
if (!existsSync(swarmDir))
|
|
60
|
+
mkdirSync(swarmDir, { recursive: true });
|
|
61
|
+
const { initializeMemoryDatabase } = await import('../memory/memory-initializer.js');
|
|
62
|
+
const result = await initializeMemoryDatabase({ force: true, verbose: false });
|
|
63
|
+
return result.success;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return runFixCommand('npx moflo memory init --force');
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
'Embeddings': async () => {
|
|
70
|
+
try {
|
|
71
|
+
const swarmDir = join(process.cwd(), '.swarm');
|
|
72
|
+
if (!existsSync(swarmDir))
|
|
73
|
+
mkdirSync(swarmDir, { recursive: true });
|
|
74
|
+
const dbPath = join(swarmDir, 'memory.db');
|
|
75
|
+
if (!existsSync(dbPath)) {
|
|
76
|
+
const { initializeMemoryDatabase } = await import('../memory/memory-initializer.js');
|
|
77
|
+
await initializeMemoryDatabase({ force: true, verbose: false });
|
|
78
|
+
}
|
|
79
|
+
return runFixCommand('npx moflo embeddings init --force');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return runFixCommand('npx moflo memory init --force');
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
'Config File': async () => {
|
|
86
|
+
try {
|
|
87
|
+
const cfDir = join(process.cwd(), '.moflo');
|
|
88
|
+
if (!existsSync(cfDir))
|
|
89
|
+
mkdirSync(cfDir, { recursive: true });
|
|
90
|
+
return runFixCommand('npx moflo config init');
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
'Daemon Status': async () => {
|
|
97
|
+
const lockFile = join(process.cwd(), '.moflo', 'daemon.lock');
|
|
98
|
+
const pidFile = join(process.cwd(), '.moflo', 'daemon.pid');
|
|
99
|
+
try {
|
|
100
|
+
if (existsSync(lockFile))
|
|
101
|
+
unlinkSync(lockFile);
|
|
102
|
+
if (existsSync(pidFile))
|
|
103
|
+
unlinkSync(pidFile);
|
|
104
|
+
}
|
|
105
|
+
catch { /* best effort */ }
|
|
106
|
+
return runFixCommand('npx moflo daemon start');
|
|
107
|
+
},
|
|
108
|
+
'MCP Servers': async () => {
|
|
109
|
+
return runFixCommand('claude mcp add moflo -- npx -y moflo mcp start');
|
|
110
|
+
},
|
|
111
|
+
'Claude Code CLI': async () => {
|
|
112
|
+
return installClaudeCode();
|
|
113
|
+
},
|
|
114
|
+
'Zombie Processes': async () => {
|
|
115
|
+
const result = await findZombieProcesses(true);
|
|
116
|
+
return result.killed > 0 || result.details.length === 0;
|
|
117
|
+
},
|
|
118
|
+
'Gate Health': async () => {
|
|
119
|
+
return fixGateHealthHooks();
|
|
120
|
+
},
|
|
121
|
+
'Status Line': async () => {
|
|
122
|
+
const settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
|
123
|
+
if (!existsSync(settingsPath))
|
|
124
|
+
return false;
|
|
125
|
+
try {
|
|
126
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
127
|
+
if (!settings.statusLine) {
|
|
128
|
+
settings.statusLine = {
|
|
129
|
+
type: 'command',
|
|
130
|
+
command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/statusline.cjs" --compact',
|
|
131
|
+
};
|
|
132
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
const fixFn = fixActions[check.name];
|
|
142
|
+
if (fixFn) {
|
|
143
|
+
try {
|
|
144
|
+
output.writeln(output.dim(` Fixing: ${check.name}...`));
|
|
145
|
+
const success = await fixFn();
|
|
146
|
+
if (success) {
|
|
147
|
+
output.writeln(output.success(` Fixed: ${check.name}`));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
output.writeln(output.warning(` Fix attempted but may need manual action: ${check.fix}`));
|
|
151
|
+
}
|
|
152
|
+
return success;
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
output.writeln(output.warning(` Fix failed: ${errorDetail(e)}`));
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Generic: try running the fix command directly if it looks like a shell command
|
|
160
|
+
if (check.fix.startsWith('npx ') || check.fix.startsWith('npm ') || check.fix.startsWith('claude ')) {
|
|
161
|
+
return runFixCommand(check.fix);
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=doctor-fixes.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor check registry: ordered list and component-name lookup.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from `doctor.ts` so the orchestration file stays small and the
|
|
5
|
+
* registry can be inspected/extended without re-touching command-action code.
|
|
6
|
+
*/
|
|
7
|
+
import { checkSubagentHealth, checkSpellExecution, checkMcpToolInvocation, checkHookExecution, checkMcpSpellIntegration, checkGateHealth, checkHookBlockDrift, checkMofloDbBridge, } from './doctor-checks-deep.js';
|
|
8
|
+
import { checkEmbeddingHygiene } from './doctor-embedding-hygiene.js';
|
|
9
|
+
import { checkSwarmFunctional, checkHiveMindFunctional, } from './doctor-checks-swarm.js';
|
|
10
|
+
import { checkMemoryAccessFunctional } from './doctor-checks-memory-access.js';
|
|
11
|
+
import { checkBuildTools, checkClaudeCode, checkDiskSpace, checkGit, checkGitRepo, checkNodeVersion, checkNpmVersion, } from './doctor-checks-runtime.js';
|
|
12
|
+
import { checkConfigFile, checkDaemonStatus, checkMcpServers, checkMemoryDatabase, checkMofloYamlCompliance, checkStatusLine, checkTestDirs, } from './doctor-checks-config.js';
|
|
13
|
+
import { checkSpellEngine, checkSandboxTier } from './doctor-checks-platform.js';
|
|
14
|
+
import { checkEmbeddings, checkSemanticQuality, } from './doctor-checks-memory.js';
|
|
15
|
+
import { checkIntelligence } from './doctor-checks-intelligence.js';
|
|
16
|
+
import { checkVersionFreshness } from './doctor-version.js';
|
|
17
|
+
import { checkZombieProcesses } from './doctor-zombies.js';
|
|
18
|
+
/** Order matters — top entries surface first under the spinner. */
|
|
19
|
+
export const allChecks = [
|
|
20
|
+
checkVersionFreshness,
|
|
21
|
+
checkNodeVersion,
|
|
22
|
+
checkNpmVersion,
|
|
23
|
+
checkClaudeCode,
|
|
24
|
+
checkGit,
|
|
25
|
+
checkGitRepo,
|
|
26
|
+
checkConfigFile,
|
|
27
|
+
checkMofloYamlCompliance,
|
|
28
|
+
checkStatusLine,
|
|
29
|
+
checkDaemonStatus,
|
|
30
|
+
checkMemoryDatabase,
|
|
31
|
+
checkEmbeddings,
|
|
32
|
+
checkEmbeddingHygiene,
|
|
33
|
+
checkTestDirs,
|
|
34
|
+
checkMcpServers,
|
|
35
|
+
checkDiskSpace,
|
|
36
|
+
checkBuildTools,
|
|
37
|
+
checkSemanticQuality,
|
|
38
|
+
checkIntelligence,
|
|
39
|
+
checkSpellEngine,
|
|
40
|
+
checkZombieProcesses,
|
|
41
|
+
checkSubagentHealth,
|
|
42
|
+
checkSpellExecution,
|
|
43
|
+
checkMcpToolInvocation,
|
|
44
|
+
checkMcpSpellIntegration,
|
|
45
|
+
checkHookExecution,
|
|
46
|
+
checkGateHealth,
|
|
47
|
+
checkHookBlockDrift,
|
|
48
|
+
checkMofloDbBridge,
|
|
49
|
+
// Issue #818 / epic #798 — coordinator-path tripwires. They share the
|
|
50
|
+
// singleton coordinator with checkSubagentHealth above and assert by
|
|
51
|
+
// agent-id (not absolute counts) so they tolerate the parallel batch.
|
|
52
|
+
checkSwarmFunctional,
|
|
53
|
+
checkHiveMindFunctional,
|
|
54
|
+
// Issue #844 — memory_store + memory_search round-trip across subagent,
|
|
55
|
+
// swarm-agent, and hive-mind contexts.
|
|
56
|
+
checkMemoryAccessFunctional,
|
|
57
|
+
checkSandboxTier,
|
|
58
|
+
];
|
|
59
|
+
/** Lookup table for `flo doctor -c <name>`. */
|
|
60
|
+
export const componentMap = {
|
|
61
|
+
'version': checkVersionFreshness,
|
|
62
|
+
'freshness': checkVersionFreshness,
|
|
63
|
+
'node': checkNodeVersion,
|
|
64
|
+
'npm': checkNpmVersion,
|
|
65
|
+
'claude': checkClaudeCode,
|
|
66
|
+
'config': checkConfigFile,
|
|
67
|
+
'yaml': checkMofloYamlCompliance,
|
|
68
|
+
'moflo-yaml': checkMofloYamlCompliance,
|
|
69
|
+
'statusline': checkStatusLine,
|
|
70
|
+
'status-line': checkStatusLine,
|
|
71
|
+
'daemon': checkDaemonStatus,
|
|
72
|
+
'memory': checkMemoryDatabase,
|
|
73
|
+
'embeddings': checkEmbeddings,
|
|
74
|
+
'embedding-hygiene': checkEmbeddingHygiene,
|
|
75
|
+
'hygiene': checkEmbeddingHygiene,
|
|
76
|
+
'git': checkGit,
|
|
77
|
+
'mcp': checkMcpServers,
|
|
78
|
+
'disk': checkDiskSpace,
|
|
79
|
+
'typescript': checkBuildTools,
|
|
80
|
+
'tests': checkTestDirs,
|
|
81
|
+
'semantic': checkSemanticQuality,
|
|
82
|
+
'quality': checkSemanticQuality,
|
|
83
|
+
'intelligence': checkIntelligence,
|
|
84
|
+
'workflows': checkSpellEngine,
|
|
85
|
+
'workflow': checkSpellEngine,
|
|
86
|
+
'subagent': checkSubagentHealth,
|
|
87
|
+
'subagents': checkSubagentHealth,
|
|
88
|
+
'agents': checkSubagentHealth,
|
|
89
|
+
'spell-exec': checkSpellExecution,
|
|
90
|
+
'mcp-tools': checkMcpToolInvocation,
|
|
91
|
+
'mcp-spell': checkMcpSpellIntegration,
|
|
92
|
+
'hooks': checkHookExecution,
|
|
93
|
+
'gates': checkGateHealth,
|
|
94
|
+
'gate': checkGateHealth,
|
|
95
|
+
'hook-drift': checkHookBlockDrift,
|
|
96
|
+
'drift': checkHookBlockDrift,
|
|
97
|
+
'sandbox': checkSandboxTier,
|
|
98
|
+
'sandbox-tier': checkSandboxTier,
|
|
99
|
+
'moflodb': checkMofloDbBridge,
|
|
100
|
+
'bridge': checkMofloDbBridge,
|
|
101
|
+
'swarm': checkSwarmFunctional,
|
|
102
|
+
'swarm-functional': checkSwarmFunctional,
|
|
103
|
+
'hive': checkHiveMindFunctional,
|
|
104
|
+
'hive-mind': checkHiveMindFunctional,
|
|
105
|
+
'hive-mind-functional': checkHiveMindFunctional,
|
|
106
|
+
'memory-access': checkMemoryAccessFunctional,
|
|
107
|
+
'memory-functional': checkMemoryAccessFunctional,
|
|
108
|
+
};
|
|
109
|
+
//# sourceMappingURL=doctor-registry.js.map
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output rendering helpers for `flo doctor`:
|
|
3
|
+
* formatted summary, JSON output, auto-fix loop, kill-zombies banner.
|
|
4
|
+
*/
|
|
5
|
+
import { output } from '../output.js';
|
|
6
|
+
import { autoFixCheck } from './doctor-fixes.js';
|
|
7
|
+
import { checkClaudeCode, installClaudeCode } from './doctor-checks-runtime.js';
|
|
8
|
+
import { findZombieProcesses, formatZombieDetail, killTrackedProcesses, } from './doctor-zombies.js';
|
|
9
|
+
export function formatCheck(check) {
|
|
10
|
+
const icon = check.status === 'pass' ? output.success('✓') :
|
|
11
|
+
check.status === 'warn' ? output.warning('⚠') :
|
|
12
|
+
output.error('✗');
|
|
13
|
+
return `${icon} ${check.name}: ${check.message}`;
|
|
14
|
+
}
|
|
15
|
+
function tally(results) {
|
|
16
|
+
return {
|
|
17
|
+
passed: results.filter(r => r.status === 'pass').length,
|
|
18
|
+
warnings: results.filter(r => r.status === 'warn').length,
|
|
19
|
+
failed: results.filter(r => r.status === 'fail').length,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export async function runKillZombiesBanner() {
|
|
23
|
+
output.writeln(output.bold('Zombie Process Scan'));
|
|
24
|
+
output.writeln();
|
|
25
|
+
const registryKilled = killTrackedProcesses();
|
|
26
|
+
if (registryKilled > 0) {
|
|
27
|
+
output.writeln(output.success(` Killed ${registryKilled} tracked background process(es) from registry`));
|
|
28
|
+
}
|
|
29
|
+
// Single OS-level scan + kill — the previous flow scanned twice.
|
|
30
|
+
const result = await findZombieProcesses(true);
|
|
31
|
+
const found = result.details.length;
|
|
32
|
+
if (found === 0) {
|
|
33
|
+
if (registryKilled === 0) {
|
|
34
|
+
output.writeln(output.success(' No orphaned moflo processes found'));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
output.writeln(output.warning(` Found ${found} additional orphaned process(es):`));
|
|
39
|
+
for (const d of result.details) {
|
|
40
|
+
output.writeln(output.dim(` ${formatZombieDetail(d)}`));
|
|
41
|
+
}
|
|
42
|
+
if (result.killed > 0) {
|
|
43
|
+
output.writeln(output.success(` Killed ${result.killed} zombie process(es)`));
|
|
44
|
+
}
|
|
45
|
+
if (result.killed < found) {
|
|
46
|
+
output.writeln(output.warning(` ${found - result.killed} process(es) could not be killed`));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
output.writeln();
|
|
50
|
+
output.writeln(output.dim('─'.repeat(50)));
|
|
51
|
+
output.writeln();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Issue #818: machine-readable output. Emits a single JSON document with
|
|
55
|
+
* per-check fields (and any FunctionalCheckDetail entries from the swarm/
|
|
56
|
+
* hive checks) and exits with the right code. Skips auto-fix entirely —
|
|
57
|
+
* --json is read-only by intent so CI gates can consume it without
|
|
58
|
+
* mutating the working tree.
|
|
59
|
+
*/
|
|
60
|
+
export function emitJsonOutput({ results, strict, allowWarnList }) {
|
|
61
|
+
const { passed, warnings, failed } = tally(results);
|
|
62
|
+
const allowSet = new Set(allowWarnList);
|
|
63
|
+
const strictWarningFailures = strict
|
|
64
|
+
? results.filter(r => r.status === 'warn' && !allowSet.has(r.name)).map(r => r.name)
|
|
65
|
+
: [];
|
|
66
|
+
const exitCode = failed > 0 || strictWarningFailures.length > 0 ? 1 : 0;
|
|
67
|
+
process.stdout.write(JSON.stringify({
|
|
68
|
+
summary: { passed, warnings, failed },
|
|
69
|
+
strict: strict ? { strictMode: true, warningsTriggeringFail: strictWarningFailures } : { strictMode: false },
|
|
70
|
+
results,
|
|
71
|
+
}, null, 2) + '\n');
|
|
72
|
+
return { success: exitCode === 0, exitCode, data: { passed, warnings, failed, results } };
|
|
73
|
+
}
|
|
74
|
+
/** Re-runs Claude Code CLI install + check if --install was passed and the prior result wasn't pass. */
|
|
75
|
+
export async function maybeAutoInstallClaudeCode(results, fixes) {
|
|
76
|
+
const claudeCodeResult = results.find(r => r.name === 'Claude Code CLI');
|
|
77
|
+
if (!claudeCodeResult || claudeCodeResult.status === 'pass')
|
|
78
|
+
return;
|
|
79
|
+
const installed = await installClaudeCode();
|
|
80
|
+
if (!installed)
|
|
81
|
+
return;
|
|
82
|
+
const newCheck = await checkClaudeCode();
|
|
83
|
+
const idx = results.findIndex(r => r.name === 'Claude Code CLI');
|
|
84
|
+
if (idx !== -1) {
|
|
85
|
+
results[idx] = newCheck;
|
|
86
|
+
const fixIdx = fixes.findIndex(f => f.startsWith('Claude Code CLI:'));
|
|
87
|
+
if (fixIdx !== -1 && newCheck.status === 'pass') {
|
|
88
|
+
fixes.splice(fixIdx, 1);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
output.writeln(formatCheck(newCheck));
|
|
92
|
+
}
|
|
93
|
+
export function renderSummary(results) {
|
|
94
|
+
const counts = tally(results);
|
|
95
|
+
output.writeln();
|
|
96
|
+
output.writeln(output.dim('─'.repeat(50)));
|
|
97
|
+
output.writeln();
|
|
98
|
+
const summaryParts = [
|
|
99
|
+
output.success(`${counts.passed} passed`),
|
|
100
|
+
counts.warnings > 0 ? output.warning(`${counts.warnings} warnings`) : null,
|
|
101
|
+
counts.failed > 0 ? output.error(`${counts.failed} failed`) : null,
|
|
102
|
+
].filter(Boolean);
|
|
103
|
+
output.writeln(`Summary: ${summaryParts.join(', ')}`);
|
|
104
|
+
return counts;
|
|
105
|
+
}
|
|
106
|
+
/** Auto-fix loop, including the post-fix re-run. Mutates `results` and `fixes` in place when fixes succeed. */
|
|
107
|
+
export async function runAutoFix(results, fixes, checksToRun) {
|
|
108
|
+
if (fixes.length === 0)
|
|
109
|
+
return;
|
|
110
|
+
output.writeln();
|
|
111
|
+
output.writeln(output.bold('Auto-fixing issues...'));
|
|
112
|
+
output.writeln();
|
|
113
|
+
const fixableResults = results.filter(r => r.fix && (r.status === 'fail' || r.status === 'warn'));
|
|
114
|
+
let fixed = 0;
|
|
115
|
+
const unfixed = [];
|
|
116
|
+
for (const check of fixableResults) {
|
|
117
|
+
const success = await autoFixCheck(check);
|
|
118
|
+
if (success) {
|
|
119
|
+
fixed++;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
unfixed.push(`${check.name}: ${check.fix}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (fixed > 0) {
|
|
126
|
+
output.writeln();
|
|
127
|
+
output.writeln(output.success(`Auto-fixed ${fixed} issue${fixed > 1 ? 's' : ''}`));
|
|
128
|
+
}
|
|
129
|
+
if (unfixed.length > 0) {
|
|
130
|
+
output.writeln();
|
|
131
|
+
output.writeln(output.bold('Manual fixes needed:'));
|
|
132
|
+
for (const fix of unfixed) {
|
|
133
|
+
output.writeln(output.dim(` ${fix}`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (fixed === 0)
|
|
137
|
+
return;
|
|
138
|
+
output.writeln();
|
|
139
|
+
output.writeln(output.dim('Re-checking...'));
|
|
140
|
+
output.writeln();
|
|
141
|
+
const reResults = await Promise.allSettled(checksToRun.map(check => check()));
|
|
142
|
+
let rePassed = 0, reWarnings = 0, reFailed = 0;
|
|
143
|
+
for (const sr of reResults) {
|
|
144
|
+
if (sr.status === 'fulfilled') {
|
|
145
|
+
output.writeln(formatCheck(sr.value));
|
|
146
|
+
if (sr.value.status === 'pass')
|
|
147
|
+
rePassed++;
|
|
148
|
+
else if (sr.value.status === 'warn')
|
|
149
|
+
reWarnings++;
|
|
150
|
+
else
|
|
151
|
+
reFailed++;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
output.writeln();
|
|
155
|
+
output.writeln(output.dim('─'.repeat(50)));
|
|
156
|
+
const reSummary = [
|
|
157
|
+
output.success(`${rePassed} passed`),
|
|
158
|
+
reWarnings > 0 ? output.warning(`${reWarnings} warnings`) : null,
|
|
159
|
+
reFailed > 0 ? output.error(`${reFailed} failed`) : null,
|
|
160
|
+
].filter(Boolean);
|
|
161
|
+
output.writeln(`After fix: ${reSummary.join(', ')}`);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Build the final CommandResult based on pass/warn/fail counts and --strict
|
|
165
|
+
* mode. Issue #784: in strict mode any non-allowlisted warning fails the run.
|
|
166
|
+
* Equality (not substring) match — an allowlist entry tolerates exactly that
|
|
167
|
+
* check, never accidentally suppresses neighboring checks like "Git"
|
|
168
|
+
* allowlisting "Git Repository".
|
|
169
|
+
*/
|
|
170
|
+
export function finalize({ results, strict, allowWarnList }) {
|
|
171
|
+
const { passed, warnings, failed } = tally(results);
|
|
172
|
+
if (failed > 0) {
|
|
173
|
+
output.writeln();
|
|
174
|
+
output.writeln(output.error('Some checks failed. Please address the issues above.'));
|
|
175
|
+
return { success: false, exitCode: 1, data: { passed, warnings, failed, results } };
|
|
176
|
+
}
|
|
177
|
+
if (warnings > 0) {
|
|
178
|
+
if (strict) {
|
|
179
|
+
const warnResults = results.filter((r) => r.status === 'warn');
|
|
180
|
+
const allowSet = new Set(allowWarnList);
|
|
181
|
+
const offending = warnResults.filter((r) => !r.name || !allowSet.has(r.name));
|
|
182
|
+
if (offending.length > 0) {
|
|
183
|
+
output.writeln();
|
|
184
|
+
output.writeln(output.error(`--strict: ${offending.length} warning${offending.length > 1 ? 's' : ''} not allowlisted ` +
|
|
185
|
+
`(use --allow-warn "<name>,<name>" to tolerate intentional warnings):`));
|
|
186
|
+
for (const r of offending) {
|
|
187
|
+
output.writeln(output.error(` ✗ ${r.name}: ${r.message ?? ''}`));
|
|
188
|
+
}
|
|
189
|
+
return { success: false, exitCode: 1, data: { passed, warnings, failed, results } };
|
|
190
|
+
}
|
|
191
|
+
output.writeln();
|
|
192
|
+
output.writeln(output.success(`--strict: ${warnResults.length} warning${warnResults.length > 1 ? 's' : ''} all allowlisted (--allow-warn).`));
|
|
193
|
+
return { success: true, data: { passed, warnings, failed, results } };
|
|
194
|
+
}
|
|
195
|
+
output.writeln();
|
|
196
|
+
output.writeln(output.warning('All checks passed with some warnings.'));
|
|
197
|
+
return { success: true, data: { passed, warnings, failed, results } };
|
|
198
|
+
}
|
|
199
|
+
output.writeln();
|
|
200
|
+
output.writeln(output.success('All checks passed! System is healthy.'));
|
|
201
|
+
return { success: true, data: { passed, warnings, failed, results } };
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=doctor-render.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared type declarations for the doctor command tree.
|
|
3
|
+
*
|
|
4
|
+
* Imported as `import type { HealthCheck } from './doctor-types.js'` so the
|
|
5
|
+
* import is erased at runtime — keeps the doctor-registry → doctor-checks-*
|
|
6
|
+
* module graph free of value-level cycles.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=doctor-types.js.map
|