moflo 4.9.13 → 4.9.15
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 +1 -0
- package/.claude/guidance/shipped/moflo-verbose-command-filtering.md +45 -0
- package/.claude/helpers/gate.cjs +21 -5
- package/.claude/helpers/simplify-classify.cjs +211 -0
- package/.claude/skills/eldar/SKILL.md +13 -8
- package/.claude/skills/fl/phases.md +18 -2
- package/.claude/skills/guidance/SKILL.md +1 -1
- package/.claude/skills/simplify/SKILL.md +35 -48
- package/.claude/skills/spell-schedule/SKILL.md +1 -1
- 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-deep.js +40 -2
- 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,211 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* /simplify diff classifier — issue #908.
|
|
4
|
+
*
|
|
5
|
+
* Decides which review tier the current diff warrants and returns a JSON
|
|
6
|
+
* dispatch decision. The /simplify skill MUST call this first so routing is
|
|
7
|
+
* deterministic and unit-testable instead of a prose decision Claude makes
|
|
8
|
+
* over and over per run.
|
|
9
|
+
*
|
|
10
|
+
* Rule (per user direction): default to single-agent Sonnet review. Only
|
|
11
|
+
* escalate to a 3-agent fan-out when diff signals genuinely warrant it.
|
|
12
|
+
* Opus is never selected — the existing skill already documents that.
|
|
13
|
+
*
|
|
14
|
+
* Outputs JSON:
|
|
15
|
+
* {
|
|
16
|
+
* "tier": "TRIVIAL" | "SMALL" | "NORMAL",
|
|
17
|
+
* "model": "sonnet",
|
|
18
|
+
* "agentCount": 0 | 1 | 3,
|
|
19
|
+
* "reasoning": [string, ...],
|
|
20
|
+
* "stats": { added, deleted, fileCount, declAdded, declRemoved, ... }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* node bin/simplify-classify.cjs [--base main]
|
|
25
|
+
* node bin/simplify-classify.cjs --diff <unified-diff-on-stdin>
|
|
26
|
+
*
|
|
27
|
+
* The --diff stdin form exists so unit tests can drive the classifier
|
|
28
|
+
* with synthetic diffs (no git repo required).
|
|
29
|
+
*/
|
|
30
|
+
'use strict';
|
|
31
|
+
|
|
32
|
+
const { execSync } = require('child_process');
|
|
33
|
+
|
|
34
|
+
// Paths where new logic warrants the 3-agent fan-out (issue #908).
|
|
35
|
+
// Mechanical edits inside these paths are still SMALL; only adding/removing
|
|
36
|
+
// declarations triggers escalation.
|
|
37
|
+
const SECURITY_PATHS = [
|
|
38
|
+
/(?:^|[\\\/])aidefence[\\\/]/i,
|
|
39
|
+
/(?:^|[\\\/])swarm[\\\/]consensus[\\\/]/i,
|
|
40
|
+
/(?:^|[\\\/])hooks?[\\\/](?:handlers?|gate|wiring)/i,
|
|
41
|
+
/(?:^|[\\\/])services[\\\/]daemon-lock\.ts$/i,
|
|
42
|
+
/(?:^|[\\\/])bin[\\\/]gate\./i,
|
|
43
|
+
/(?:^|[\\\/])bin[\\\/]session-start-launcher\./i,
|
|
44
|
+
/(?:^|[\\\/])\.claude[\\\/]helpers[\\\/]gate/i,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function safeExec(cmd) {
|
|
48
|
+
try { return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }); }
|
|
49
|
+
catch { return ''; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readDiffFromGit(base) {
|
|
53
|
+
// Combined diff: committed-since-base + working-tree
|
|
54
|
+
const committed = safeExec(`git diff ${base}...HEAD`);
|
|
55
|
+
const working = safeExec('git diff HEAD');
|
|
56
|
+
return committed + (working ? '\n' + working : '');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse a unified-diff string into per-file stats and aggregate signals.
|
|
61
|
+
* No git/I/O — pure function over the diff text. Test-friendly.
|
|
62
|
+
*/
|
|
63
|
+
function parseDiff(diff) {
|
|
64
|
+
const lines = diff.split('\n');
|
|
65
|
+
const files = new Map(); // filename → { added, deleted, declAdded, declRemoved, isNew, isRenamed }
|
|
66
|
+
let current = null;
|
|
67
|
+
|
|
68
|
+
// Match function/class/export-const-arrow/method declarations being
|
|
69
|
+
// added or removed. Conservative — biased toward false negatives so we
|
|
70
|
+
// don't over-escalate.
|
|
71
|
+
const DECL_RE = /^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function|class|interface|type)\s+\w/;
|
|
72
|
+
const ARROW_DECL_RE = /^(?:export\s+)?(?:const|let|var)\s+\w+\s*[:=].*=>\s*\{?$/;
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < lines.length; i++) {
|
|
75
|
+
const ln = lines[i];
|
|
76
|
+
|
|
77
|
+
// File header: `diff --git a/path b/path`
|
|
78
|
+
let m = ln.match(/^diff --git (?:a\/)?(.+?) (?:b\/)?(.+)$/);
|
|
79
|
+
if (m) {
|
|
80
|
+
const filename = m[2];
|
|
81
|
+
current = { filename, added: 0, deleted: 0, declAdded: 0, declRemoved: 0, isNew: false, isRenamed: false };
|
|
82
|
+
files.set(filename, current);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!current) continue;
|
|
86
|
+
|
|
87
|
+
if (ln.startsWith('new file mode')) current.isNew = true;
|
|
88
|
+
if (ln.startsWith('rename from') || ln.startsWith('rename to') || ln.startsWith('similarity index')) current.isRenamed = true;
|
|
89
|
+
|
|
90
|
+
// Skip diff headers
|
|
91
|
+
if (ln.startsWith('+++') || ln.startsWith('---') || ln.startsWith('@@') || ln.startsWith('index ')) continue;
|
|
92
|
+
|
|
93
|
+
if (ln.startsWith('+') && !ln.startsWith('+++')) {
|
|
94
|
+
current.added++;
|
|
95
|
+
const body = ln.slice(1).trim();
|
|
96
|
+
if (DECL_RE.test(body) || ARROW_DECL_RE.test(body)) current.declAdded++;
|
|
97
|
+
} else if (ln.startsWith('-') && !ln.startsWith('---')) {
|
|
98
|
+
current.deleted++;
|
|
99
|
+
const body = ln.slice(1).trim();
|
|
100
|
+
if (DECL_RE.test(body) || ARROW_DECL_RE.test(body)) current.declRemoved++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Aggregate
|
|
105
|
+
let added = 0, deleted = 0, declAdded = 0, declRemoved = 0;
|
|
106
|
+
let newFiles = 0, renamedFiles = 0;
|
|
107
|
+
let securityHit = false;
|
|
108
|
+
for (const f of files.values()) {
|
|
109
|
+
added += f.added;
|
|
110
|
+
deleted += f.deleted;
|
|
111
|
+
declAdded += f.declAdded;
|
|
112
|
+
declRemoved += f.declRemoved;
|
|
113
|
+
if (f.isNew) newFiles++;
|
|
114
|
+
if (f.isRenamed) renamedFiles++;
|
|
115
|
+
if (SECURITY_PATHS.some(rx => rx.test(f.filename))) securityHit = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
added, deleted, declAdded, declRemoved,
|
|
120
|
+
netDecls: declAdded - declRemoved,
|
|
121
|
+
fileCount: files.size,
|
|
122
|
+
newFiles, renamedFiles,
|
|
123
|
+
securityHit,
|
|
124
|
+
files: [...files.keys()],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Pure decision function. Takes parsed stats, returns dispatch decision.
|
|
130
|
+
* No I/O. Easy to unit-test with synthetic stats.
|
|
131
|
+
*/
|
|
132
|
+
function decide(stats) {
|
|
133
|
+
const reasoning = [];
|
|
134
|
+
const totalChange = stats.added + stats.deleted;
|
|
135
|
+
|
|
136
|
+
if (totalChange === 0) {
|
|
137
|
+
return { tier: 'TRIVIAL', model: 'sonnet', agentCount: 0, reasoning: ['empty diff — nothing to review'], stats };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// TRIVIAL: tiny diff, no declarations changed
|
|
141
|
+
if (totalChange <= 10 && stats.fileCount <= 1 && stats.netDecls === 0 && stats.declAdded === 0 && stats.declRemoved === 0) {
|
|
142
|
+
reasoning.push(`≤10 LOC in 1 file with no declaration changes`);
|
|
143
|
+
return { tier: 'TRIVIAL', model: 'sonnet', agentCount: 0, reasoning, stats };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Mechanical relocation detection — the #906 case.
|
|
147
|
+
// If declarations were both ADDED and REMOVED at roughly matching rates,
|
|
148
|
+
// it's a structural move, not net-new logic. Judge by declaration balance,
|
|
149
|
+
// not raw LOC balance — formatting/blank-line differences between source
|
|
150
|
+
// and destination files easily push raw LOC out of balance even when the
|
|
151
|
+
// semantic change is purely "moved 5 functions across 5 new files".
|
|
152
|
+
// Mechanical relocations are SMALL even when many files / many lines.
|
|
153
|
+
const declTouched = stats.declAdded + stats.declRemoved;
|
|
154
|
+
const isMostlyRelocation = stats.declAdded >= 2
|
|
155
|
+
&& stats.declRemoved >= 2
|
|
156
|
+
&& Math.abs(stats.netDecls) <= Math.max(2, Math.floor(declTouched * 0.30));
|
|
157
|
+
|
|
158
|
+
if (isMostlyRelocation) {
|
|
159
|
+
reasoning.push(
|
|
160
|
+
`mostly relocation: ${stats.declAdded} decls added, ${stats.declRemoved} removed, net ${stats.netDecls >= 0 ? '+' : ''}${stats.netDecls}`,
|
|
161
|
+
);
|
|
162
|
+
return { tier: 'SMALL', model: 'sonnet', agentCount: 1, reasoning, stats };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Escalation triggers — any one trips NORMAL (3 agents).
|
|
166
|
+
// Always Sonnet — Opus is never the right model for /simplify per skill rule.
|
|
167
|
+
const triggers = [];
|
|
168
|
+
if (totalChange > 500) triggers.push(`>500 LOC changed (${totalChange})`);
|
|
169
|
+
if (stats.fileCount >= 5 && stats.netDecls >= 3) triggers.push(`${stats.fileCount} files with ${stats.netDecls} net new declarations`);
|
|
170
|
+
if (stats.securityHit && stats.netDecls > 0) triggers.push('security-sensitive path with new logic');
|
|
171
|
+
if (stats.newFiles >= 3 && stats.declAdded >= 5) triggers.push(`${stats.newFiles} new files with ${stats.declAdded} new declarations`);
|
|
172
|
+
|
|
173
|
+
if (triggers.length > 0) {
|
|
174
|
+
return { tier: 'NORMAL', model: 'sonnet', agentCount: 3, reasoning: triggers, stats };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Default: SMALL — single sonnet agent
|
|
178
|
+
reasoning.push(`small/medium diff: ${totalChange} LOC across ${stats.fileCount} file(s), +${stats.declAdded}/-${stats.declRemoved} decls`);
|
|
179
|
+
return { tier: 'SMALL', model: 'sonnet', agentCount: 1, reasoning, stats };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function classifyDiff(diffText) {
|
|
183
|
+
return decide(parseDiff(diffText));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function classifyFromGit(base = 'main') {
|
|
187
|
+
return classifyDiff(readDiffFromGit(base));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (require.main === module) {
|
|
191
|
+
const args = process.argv.slice(2);
|
|
192
|
+
const baseIdx = args.indexOf('--base');
|
|
193
|
+
const base = baseIdx >= 0 ? args[baseIdx + 1] : 'main';
|
|
194
|
+
const stdinDiff = args.includes('--diff') || args.includes('--stdin');
|
|
195
|
+
|
|
196
|
+
let result;
|
|
197
|
+
if (stdinDiff) {
|
|
198
|
+
let buf = '';
|
|
199
|
+
process.stdin.setEncoding('utf-8');
|
|
200
|
+
process.stdin.on('data', (d) => { buf += d; });
|
|
201
|
+
process.stdin.on('end', () => {
|
|
202
|
+
result = classifyDiff(buf);
|
|
203
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
204
|
+
});
|
|
205
|
+
} else {
|
|
206
|
+
result = classifyFromGit(base);
|
|
207
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { parseDiff, decide, classifyDiff, classifyFromGit };
|
|
@@ -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
|
|
@@ -432,6 +432,31 @@ const REQUIRED_GATE_CASES = [
|
|
|
432
432
|
// session-start-launcher.mjs in consumer projects without transitive failures.
|
|
433
433
|
import { REQUIRED_HOOK_WIRING } from '../services/hook-wiring.js';
|
|
434
434
|
export { REQUIRED_HOOK_WIRING };
|
|
435
|
+
/**
|
|
436
|
+
* Detect "expected pre-publish drift" — source `bin/gate.cjs` is ahead of the
|
|
437
|
+
* installed `node_modules/moflo/bin/gate.cjs`, but the deployed
|
|
438
|
+
* `.claude/helpers/gate.cjs` still matches the installed version. This is the
|
|
439
|
+
* steady state in the moflo dogfood repo while a PR has landed but no
|
|
440
|
+
* `npm install moflo@<new>` has rotated the package.
|
|
441
|
+
*
|
|
442
|
+
* Returns true only when both are true:
|
|
443
|
+
* - helper content equals installed bin content (helper is correctly synced
|
|
444
|
+
* to what's installed)
|
|
445
|
+
* - installed bin content differs from source bin content (source is ahead)
|
|
446
|
+
*
|
|
447
|
+
* If `node_modules/moflo/bin/gate.cjs` is missing (consumer never installed
|
|
448
|
+
* moflo, or path is unusual) we conservatively return false so other drift
|
|
449
|
+
* detection still applies.
|
|
450
|
+
*/
|
|
451
|
+
export function isExpectedPrePublishDrift(installedBinGate, helperContent, sourceBinContent) {
|
|
452
|
+
try {
|
|
453
|
+
const installedContent = readFileSync(installedBinGate, 'utf8');
|
|
454
|
+
return installedContent === helperContent && installedContent !== sourceBinContent;
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
435
460
|
/**
|
|
436
461
|
* Verify gate infrastructure health:
|
|
437
462
|
* 1. gate.cjs exists and contains all required cases
|
|
@@ -474,14 +499,27 @@ export async function checkGateHealth() {
|
|
|
474
499
|
issues.push(`gate.cjs missing cases: ${missingCases.join(', ')}`);
|
|
475
500
|
}
|
|
476
501
|
// 2. Check bin/gate.cjs sync
|
|
502
|
+
//
|
|
503
|
+
// The launcher syncs `node_modules/moflo/bin/gate.cjs` → `.claude/helpers/gate.cjs`
|
|
504
|
+
// on version change. Source `bin/gate.cjs` is only present in the moflo dogfood
|
|
505
|
+
// repo. During the dogfood publish window — between a PR landing and the next
|
|
506
|
+
// `npm install moflo@<new>` — source bin/ legitimately moves ahead of the
|
|
507
|
+
// installed bin/, while the helper continues to mirror the installed version.
|
|
508
|
+
// That's the expected steady state, not a bug; downgrade it to `warn` and skip
|
|
509
|
+
// the `fix` field so `--fix` doesn't paint a false success (#913).
|
|
477
510
|
const binGate = join(projectDir, 'bin', 'gate.cjs');
|
|
511
|
+
const installedBinGate = join(projectDir, 'node_modules', 'moflo', 'bin', 'gate.cjs');
|
|
478
512
|
if (existsSync(binGate)) {
|
|
479
513
|
try {
|
|
480
514
|
const binContent = readFileSync(binGate, 'utf8');
|
|
481
515
|
if (binContent !== gateContent) {
|
|
482
|
-
// Check if it's a size difference (likely out of sync) vs whitespace
|
|
483
516
|
const sizeDiff = Math.abs(binContent.length - gateContent.length);
|
|
484
|
-
|
|
517
|
+
const prePublishDrift = isExpectedPrePublishDrift(installedBinGate, gateContent, binContent);
|
|
518
|
+
if (prePublishDrift) {
|
|
519
|
+
warnings.push(`source bin/gate.cjs is ${sizeDiff} chars ahead of node_modules/moflo/bin/gate.cjs ` +
|
|
520
|
+
'(expected pre-publish drift; resolves on next `npm install moflo@<new>`)');
|
|
521
|
+
}
|
|
522
|
+
else if (sizeDiff > 10) {
|
|
485
523
|
issues.push(`bin/gate.cjs out of sync with .claude/helpers/gate.cjs (${sizeDiff} chars differ)`);
|
|
486
524
|
}
|
|
487
525
|
else {
|