moflo 4.10.8 → 4.10.10
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/skills/luminarium/SKILL.md +66 -0
- package/bin/hooks.mjs +3 -2
- package/bin/index-all.mjs +3 -2
- package/bin/index-guidance.mjs +4 -4
- package/bin/lib/process-manager.mjs +3 -3
- package/dist/src/cli/commands/doctor-checks-config.js +44 -10
- package/dist/src/cli/commands/doctor-fixes.js +133 -1
- package/dist/src/cli/commands/doctor-registry.js +7 -1
- package/dist/src/cli/init/executor.js +1 -0
- package/dist/src/cli/movector/model-router.js +1 -1
- package/dist/src/cli/movector/q-learning-router.js +2 -2
- package/dist/src/cli/services/daemon-dashboard.js +22 -2
- package/dist/src/cli/services/daemon-port.js +35 -0
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: luminarium
|
|
3
|
+
description: |
|
|
4
|
+
Print the localhost URL for The Luminarium — moflo's daemon dashboard — for the current project.
|
|
5
|
+
Use when the user asks for "the luminarium link", "the moflo dashboard", "the daemon UI", or anything synonymous.
|
|
6
|
+
Each project gets a deterministic port in 33000–33999; the actual bound port is recorded in `.moflo/daemon.lock`.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# /luminarium — Project Dashboard Link
|
|
10
|
+
|
|
11
|
+
Surface the URL for The Luminarium (the moflo daemon's localhost UI) for the project that this session is running in. No prompts, no confirmations — print the link and stop.
|
|
12
|
+
|
|
13
|
+
## Procedure
|
|
14
|
+
|
|
15
|
+
1. **Find the project root.** Walk up from `process.cwd()` looking for a `.moflo/` directory. The session's cwd is almost always the project root, so check there first.
|
|
16
|
+
|
|
17
|
+
2. **Read `.moflo/daemon.lock`.** It's a JSON file written by the daemon at bind time. The dashboard port is the `port` field:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{ "pid": 12345, "port": 33421, "startedAt": "...", ... }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- If the file exists and `port` is a valid number → the daemon is running and bound. Use that port.
|
|
24
|
+
- If the file is missing or `port` is absent/invalid → the daemon is not running. See step 4.
|
|
25
|
+
|
|
26
|
+
3. **Print the link** in a single line, with the path verbatim — Claude Code renders it as clickable:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
The Luminarium: http://localhost:<port>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Nothing else. No banner, no follow-up question, no "what would you like to do?".
|
|
33
|
+
|
|
34
|
+
4. **If the daemon isn't running** (no lock file, or unparseable), say so in one line and offer the start command — don't run it:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
The moflo daemon isn't running for this project. Start it with: npx flo daemon start
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Why read the lock, not compute the port
|
|
41
|
+
|
|
42
|
+
The port is project-deterministic (sha256(projectRoot) mapped into 33000–33999), but if the deterministic port was already taken at bind time the daemon scans forward and binds an alternate. The lock file is the only source of truth for what's actually bound. Do not compute the hash yourself — read the file.
|
|
43
|
+
|
|
44
|
+
## Don't
|
|
45
|
+
|
|
46
|
+
- Don't fall back to any hardcoded port — there is no project-agnostic dashboard port; a literal would route to a foreign daemon on a multi-project machine. If the lock is missing, report "not running".
|
|
47
|
+
- Don't compute the deterministic port and report it as the link when the lock is missing — the daemon may be down, or bound to an alternate port. Report "not running" instead.
|
|
48
|
+
- Don't run `flo daemon start` automatically — the user asked for a link, not for daemon management. Leave starting to `/healer` or the user.
|
|
49
|
+
- Don't open a browser. Print the URL; let the user click.
|
|
50
|
+
|
|
51
|
+
## Output
|
|
52
|
+
|
|
53
|
+
A single line. Examples:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
The Luminarium: http://localhost:33421
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
The moflo daemon isn't running for this project. Start it with: npx flo daemon start
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## See Also
|
|
64
|
+
|
|
65
|
+
- `/healer` — diagnoses and (with `--fix`) starts the daemon if it's not running.
|
|
66
|
+
- `src/cli/services/daemon-port.ts` (and its JS twin `bin/lib/daemon-port.mjs`) — canonical port-resolution helpers; `resolveClientPort()` is what the rest of moflo uses.
|
package/bin/hooks.mjs
CHANGED
|
@@ -36,14 +36,15 @@ const __dirname = dirname(__filename);
|
|
|
36
36
|
// projects, so __dirname-relative paths break. findProjectRoot() works
|
|
37
37
|
// everywhere and resolves identically to the TS bridge (see lib/moflo-paths.mjs).
|
|
38
38
|
const projectRoot = findProjectRoot();
|
|
39
|
-
const logFile = resolve(projectRoot, '.
|
|
39
|
+
const logFile = resolve(projectRoot, '.moflo', 'logs', 'hooks.log');
|
|
40
|
+
try { mkdirSync(dirname(logFile), { recursive: true }); } catch { /* best effort */ }
|
|
40
41
|
const pm = createProcessManager(projectRoot);
|
|
41
42
|
|
|
42
43
|
// Parse command line args
|
|
43
44
|
const args = process.argv.slice(2);
|
|
44
45
|
const hookType = args[0];
|
|
45
46
|
|
|
46
|
-
// Simple log function - writes to .
|
|
47
|
+
// Simple log function - writes to .moflo/logs/hooks.log
|
|
47
48
|
function log(level, message) {
|
|
48
49
|
const timestamp = new Date().toISOString();
|
|
49
50
|
const line = `[${timestamp}] [${level.toUpperCase()}] [${hookType || 'unknown'}] ${message}\n`;
|
package/bin/index-all.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Spawned as a single detached background process by hooks.mjs session-start.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { existsSync, appendFileSync, readFileSync } from 'fs';
|
|
16
|
+
import { existsSync, appendFileSync, readFileSync, mkdirSync } from 'fs';
|
|
17
17
|
import { resolve, dirname } from 'path';
|
|
18
18
|
import { fileURLToPath } from 'url';
|
|
19
19
|
import { spawn, spawnSync } from 'child_process';
|
|
@@ -43,7 +43,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
43
43
|
// so __dirname-relative paths break. findProjectRoot() (lib/moflo-paths.mjs)
|
|
44
44
|
// works in both locations and resolves identically to the TS bridge.
|
|
45
45
|
const projectRoot = findProjectRoot();
|
|
46
|
-
const LOG_PATH = resolve(projectRoot, '.
|
|
46
|
+
const LOG_PATH = resolve(projectRoot, '.moflo', 'logs', 'hooks.log');
|
|
47
|
+
try { mkdirSync(dirname(LOG_PATH), { recursive: true }); } catch { /* best effort */ }
|
|
47
48
|
|
|
48
49
|
function log(msg) {
|
|
49
50
|
const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
package/bin/index-guidance.mjs
CHANGED
|
@@ -845,16 +845,16 @@ if (!skipEmbeddings && needsEmbeddings) {
|
|
|
845
845
|
|
|
846
846
|
if (embeddingScript) {
|
|
847
847
|
// Register the spawn with the shared ProcessManager (#886). Stdout/stderr
|
|
848
|
-
// route through `.
|
|
849
|
-
//
|
|
850
|
-
//
|
|
848
|
+
// route through `.moflo/logs/background.log` (pm.spawn default) so the
|
|
849
|
+
// registry, dedup, and session-end drain stay consistent with every other
|
|
850
|
+
// tracked spawn.
|
|
851
851
|
const pm = createProcessManager(projectRoot);
|
|
852
852
|
const result = pm.spawn('node', [embeddingScript, '--namespace', NAMESPACE], `build-embeddings-${NAMESPACE}`);
|
|
853
853
|
if (result.skipped) {
|
|
854
854
|
log(`Background embedding already running (PID: ${result.pid})`);
|
|
855
855
|
} else if (result.pid) {
|
|
856
856
|
log(`Background embedding started (PID: ${result.pid})`);
|
|
857
|
-
log(`Log file: .
|
|
857
|
+
log(`Log file: .moflo/logs/background.log`);
|
|
858
858
|
} else {
|
|
859
859
|
log('⚠️ Failed to spawn background embedding');
|
|
860
860
|
}
|
|
@@ -164,9 +164,9 @@ export function createProcessManager(root) {
|
|
|
164
164
|
// This ensures errors from background indexers/pretrain are captured
|
|
165
165
|
let stdio = 'ignore';
|
|
166
166
|
try {
|
|
167
|
-
const
|
|
168
|
-
ensureDir(
|
|
169
|
-
const logPath = resolve(
|
|
167
|
+
const logsDir = resolve(projectRoot, '.moflo', 'logs');
|
|
168
|
+
ensureDir(logsDir);
|
|
169
|
+
const logPath = resolve(logsDir, 'background.log');
|
|
170
170
|
const fd = openSync(logPath, 'a');
|
|
171
171
|
stdio = ['ignore', fd, fd];
|
|
172
172
|
} catch {
|
|
@@ -7,8 +7,8 @@ import { existsSync, readFileSync, statSync } from 'fs';
|
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import os from 'os';
|
|
9
9
|
import { findProjectDaemonPids, getDaemonLockHolder, getDaemonLockPayload, } from '../services/daemon-lock.js';
|
|
10
|
-
import { resolveClientPort, LEGACY_DEFAULT_PORT,
|
|
11
|
-
import {
|
|
10
|
+
import { resolveClientPort, LEGACY_DEFAULT_PORT, probeDaemonHealthWithRetry as probeDaemonHealthIdentity, normalizeProjectRoot, } from '../services/daemon-port.js';
|
|
11
|
+
import { LEGACY_SWARM_DIR, memoryDbCandidatePaths, memoryDbPath, } from '../services/moflo-paths.js';
|
|
12
12
|
import { probeDbIntegrity } from '../services/memory-db-integrity-repair.js';
|
|
13
13
|
import { findProjectRoot } from '../services/project-root.js';
|
|
14
14
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
@@ -253,14 +253,10 @@ export async function checkMemoryDatabase() {
|
|
|
253
253
|
}
|
|
254
254
|
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
255
255
|
if (dbPath === canonical) {
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
if (existsSync(legacyMemoryDbPath(root))) {
|
|
261
|
-
message += ' — legacy .swarm/memory.db still present (delete it after confirming canonical is healthy)';
|
|
262
|
-
}
|
|
263
|
-
return { name: 'Memory Database', status: 'pass', message };
|
|
256
|
+
// Legacy `.swarm/memory.db` residue is owned by the separate
|
|
257
|
+
// `checkSwarmResidue` check so we keep this check focused on the
|
|
258
|
+
// canonical DB. That check carries the auto-fix.
|
|
259
|
+
return { name: 'Memory Database', status: 'pass', message: `.moflo/moflo.db (${sizeMB} MB)` };
|
|
264
260
|
}
|
|
265
261
|
return {
|
|
266
262
|
name: 'Memory Database',
|
|
@@ -271,6 +267,44 @@ export async function checkMemoryDatabase() {
|
|
|
271
267
|
}
|
|
272
268
|
return { name: 'Memory Database', status: 'warn', message: 'Not initialized', fix: 'claude-flow memory configure --backend hybrid' };
|
|
273
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Catches `.swarm/` residue that survived past the canonical migration:
|
|
272
|
+
* - `memory.db` / `memory.db.bak` — stale once `.moflo/moflo.db` exists.
|
|
273
|
+
* - `q-learning-model.json` / `model-router-state.json` — live router state
|
|
274
|
+
* that pre-dates the `.moflo/movector/` defaults; migrate, don't delete.
|
|
275
|
+
* - `hooks.log` / `background.log` — diagnostic logs the launcher used to
|
|
276
|
+
* route to `.swarm/`; relocate to `.moflo/logs/`.
|
|
277
|
+
*
|
|
278
|
+
* Passes when `.swarm/` is absent OR contains nothing the migrator recognises.
|
|
279
|
+
* Otherwise warns with `fix: 'flo healer --fix -c swarm-residue'` so the auto-fix
|
|
280
|
+
* dispatcher (`fixSwarmLegacyResidue` in doctor-fixes.ts) can clean it up in
|
|
281
|
+
* one pass.
|
|
282
|
+
*/
|
|
283
|
+
export async function checkSwarmResidue() {
|
|
284
|
+
const root = findProjectRoot();
|
|
285
|
+
const swarmDir = join(root, LEGACY_SWARM_DIR);
|
|
286
|
+
if (!existsSync(swarmDir)) {
|
|
287
|
+
return { name: 'Swarm Residue', status: 'pass', message: 'No .swarm/ directory present' };
|
|
288
|
+
}
|
|
289
|
+
const artifacts = [
|
|
290
|
+
'memory.db',
|
|
291
|
+
'memory.db.bak',
|
|
292
|
+
'q-learning-model.json',
|
|
293
|
+
'model-router-state.json',
|
|
294
|
+
'hooks.log',
|
|
295
|
+
'background.log',
|
|
296
|
+
];
|
|
297
|
+
const present = artifacts.filter(name => existsSync(join(swarmDir, name)));
|
|
298
|
+
if (present.length === 0) {
|
|
299
|
+
return { name: 'Swarm Residue', status: 'pass', message: '.swarm/ present but no known residue' };
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
name: 'Swarm Residue',
|
|
303
|
+
status: 'warn',
|
|
304
|
+
message: `${present.length} legacy artifact(s) in .swarm/: ${present.join(', ')}`,
|
|
305
|
+
fix: 'flo healer --fix -c swarm-residue',
|
|
306
|
+
};
|
|
307
|
+
}
|
|
274
308
|
/**
|
|
275
309
|
* Tier-1 corruption probe for `.moflo/moflo.db`. Runs `PRAGMA integrity_check`
|
|
276
310
|
* via a raw node:sqlite readonly handle — bypasses `openBackend` because that
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* shell-out where possible). Falls back to running the check's `fix` string
|
|
6
6
|
* if it looks like an `npx`/`npm`/`claude` command.
|
|
7
7
|
*/
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
8
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmdirSync, unlinkSync, writeFileSync, readdirSync } from 'fs';
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
import { output } from '../output.js';
|
|
11
11
|
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
@@ -13,6 +13,7 @@ import { atomicWriteFileSync } from '../shared/utils/atomic-file-write.js';
|
|
|
13
13
|
import { repairHookWiring } from '../services/hook-wiring.js';
|
|
14
14
|
import { getDaemonLockHolder } from '../services/daemon-lock.js';
|
|
15
15
|
import { findProjectRoot } from '../services/project-root.js';
|
|
16
|
+
import { legacyMemoryDbPath, legacyMemoryDbBakPath, memoryDbPath, mofloDir } from '../services/moflo-paths.js';
|
|
16
17
|
import { findZombieProcesses } from './doctor-zombies.js';
|
|
17
18
|
import { inspectMcpConfigs } from './doctor-checks-config.js';
|
|
18
19
|
import { installClaudeCode, runCommand } from './doctor-checks-runtime.js';
|
|
@@ -94,6 +95,131 @@ async function fixGateHealthHooks() {
|
|
|
94
95
|
}
|
|
95
96
|
return driftFixed && wiringFixed;
|
|
96
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Migrate `.swarm/` residue to its canonical home and remove the legacy directory.
|
|
100
|
+
*
|
|
101
|
+
* Three categories of artifact:
|
|
102
|
+
* 1. `memory.db` (+ `.bak`) — stale once `.moflo/moflo.db` exists; delete.
|
|
103
|
+
* 2. `q-learning-model.json` / `model-router-state.json` — live RL state.
|
|
104
|
+
* Rename into `.moflo/movector/`. If the canonical target already exists
|
|
105
|
+
* (consumer ran moflo on the new defaults before the auto-fix), the
|
|
106
|
+
* canonical wins and the `.swarm/` copy is unlinked.
|
|
107
|
+
* 3. `hooks.log` / `background.log` — diagnostic logs. Relocate to
|
|
108
|
+
* `.moflo/logs/`; append into the canonical file if it already exists
|
|
109
|
+
* so we don't drop history. Best-effort — log loss is acceptable.
|
|
110
|
+
*
|
|
111
|
+
* Finally `rmdir .swarm/` if and only if it's empty. Anything we didn't
|
|
112
|
+
* recognise is left in place rather than silently deleted.
|
|
113
|
+
*
|
|
114
|
+
* Cross-platform: uses `fs.rename`/`rmdir` (Node primitives), forward-slash-free
|
|
115
|
+
* joins via `path.join`. Works on Windows + POSIX.
|
|
116
|
+
*
|
|
117
|
+
* Returns true if the directory was fully retired OR if there was nothing to
|
|
118
|
+
* migrate; false if any relocation throws or `.swarm/` survives with unknown
|
|
119
|
+
* contents.
|
|
120
|
+
*/
|
|
121
|
+
async function fixSwarmLegacyResidue() {
|
|
122
|
+
const root = findProjectRoot();
|
|
123
|
+
const swarmDir = join(root, '.swarm');
|
|
124
|
+
if (!existsSync(swarmDir))
|
|
125
|
+
return true;
|
|
126
|
+
const canonicalDb = memoryDbPath(root);
|
|
127
|
+
const moflo = mofloDir(root);
|
|
128
|
+
const movectorDir = join(moflo, 'movector');
|
|
129
|
+
const logsDir = join(moflo, 'logs');
|
|
130
|
+
let allMigrated = true;
|
|
131
|
+
// (1) memory.db + .bak — both are migration artifacts of the launcher's
|
|
132
|
+
// copy-verify-rename step; if the canonical isn't yet in place neither
|
|
133
|
+
// source is safe to delete. The launcher creates the `.bak` only AFTER
|
|
134
|
+
// canonical exists, so this guard is conservative but correct.
|
|
135
|
+
const legacyDbPaths = [
|
|
136
|
+
['memory.db', legacyMemoryDbPath(root)],
|
|
137
|
+
['memory.db.bak', legacyMemoryDbBakPath(root)],
|
|
138
|
+
];
|
|
139
|
+
for (const [name, src] of legacyDbPaths) {
|
|
140
|
+
if (!existsSync(src))
|
|
141
|
+
continue;
|
|
142
|
+
if (!existsSync(canonicalDb)) {
|
|
143
|
+
output.writeln(output.warning(` Skipping ${name}: .moflo/moflo.db absent — run \`flo memory init\` first.`));
|
|
144
|
+
allMigrated = false;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
unlinkSync(src);
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
output.writeln(output.warning(` Failed to delete ${name}: ${errorDetail(e)}`));
|
|
152
|
+
allMigrated = false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// (2) router state JSONs — rename into .moflo/movector/.
|
|
156
|
+
const stateFiles = [
|
|
157
|
+
{ name: 'q-learning-model.json', dest: movectorDir },
|
|
158
|
+
{ name: 'model-router-state.json', dest: movectorDir },
|
|
159
|
+
];
|
|
160
|
+
for (const { name, dest } of stateFiles) {
|
|
161
|
+
const src = join(swarmDir, name);
|
|
162
|
+
if (!existsSync(src))
|
|
163
|
+
continue;
|
|
164
|
+
const target = join(dest, name);
|
|
165
|
+
try {
|
|
166
|
+
mkdirSync(dest, { recursive: true });
|
|
167
|
+
if (existsSync(target)) {
|
|
168
|
+
// Canonical already populated by a fresh save on the new defaults.
|
|
169
|
+
// Keep the canonical, drop the legacy copy.
|
|
170
|
+
unlinkSync(src);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
renameSync(src, target);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
output.writeln(output.warning(` Failed to relocate ${name}: ${errorDetail(e)}`));
|
|
178
|
+
allMigrated = false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// (3) logs — best-effort move. Append into canonical if it already exists
|
|
182
|
+
// (don't drop history). Hook + background logs are bounded to kilobytes in
|
|
183
|
+
// practice so the read-into-memory cost is acceptable.
|
|
184
|
+
const logFiles = ['hooks.log', 'background.log'];
|
|
185
|
+
for (const name of logFiles) {
|
|
186
|
+
const src = join(swarmDir, name);
|
|
187
|
+
if (!existsSync(src))
|
|
188
|
+
continue;
|
|
189
|
+
const target = join(logsDir, name);
|
|
190
|
+
try {
|
|
191
|
+
mkdirSync(logsDir, { recursive: true });
|
|
192
|
+
if (existsSync(target)) {
|
|
193
|
+
appendFileSync(target, readFileSync(src));
|
|
194
|
+
unlinkSync(src);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
renameSync(src, target);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
output.writeln(output.warning(` Failed to relocate ${name}: ${errorDetail(e)}`));
|
|
202
|
+
allMigrated = false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// (4) rmdir .swarm/ if it's empty. Anything left is unrecognised — leave it
|
|
206
|
+
// for the user to inspect rather than silently delete.
|
|
207
|
+
try {
|
|
208
|
+
const remaining = readdirSync(swarmDir);
|
|
209
|
+
if (remaining.length === 0) {
|
|
210
|
+
rmdirSync(swarmDir);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
output.writeln(output.dim(` .swarm/ kept (${remaining.length} unrecognised entr${remaining.length === 1 ? 'y' : 'ies'}): ${remaining.join(', ')}`));
|
|
214
|
+
allMigrated = false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
output.writeln(output.warning(` Failed to remove .swarm/: ${errorDetail(e)}`));
|
|
219
|
+
allMigrated = false;
|
|
220
|
+
}
|
|
221
|
+
return allMigrated;
|
|
222
|
+
}
|
|
97
223
|
/**
|
|
98
224
|
* Execute the fix for a failed/warned health check.
|
|
99
225
|
* Returns true if the fix succeeded (re-check should pass).
|
|
@@ -440,6 +566,12 @@ export async function autoFixCheck(check) {
|
|
|
440
566
|
return false;
|
|
441
567
|
}
|
|
442
568
|
},
|
|
569
|
+
// Migrate `.swarm/` residue (legacy memory.db, RL state JSONs, hook/bg logs)
|
|
570
|
+
// into their canonical `.moflo/` homes and rmdir the directory once empty.
|
|
571
|
+
// See `fixSwarmLegacyResidue` for the per-artifact policy.
|
|
572
|
+
'Swarm Residue': async () => {
|
|
573
|
+
return fixSwarmLegacyResidue();
|
|
574
|
+
},
|
|
443
575
|
'Status Line': async () => {
|
|
444
576
|
const settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
|
445
577
|
if (!existsSync(settingsPath))
|
|
@@ -12,7 +12,7 @@ import { checkWritersAudit } from './doctor-checks-writers-audit.js';
|
|
|
12
12
|
import { checkSwarmFunctional, checkHiveMindFunctional, } from './doctor-checks-swarm.js';
|
|
13
13
|
import { checkMemoryAccessFunctional } from './doctor-checks-memory-access.js';
|
|
14
14
|
import { checkBuildTools, checkClaudeCode, checkDiskSpace, checkGit, checkGitRepo, checkNodeVersion, checkNpmVersion, } from './doctor-checks-runtime.js';
|
|
15
|
-
import { checkConfigFile, checkDaemonIdentity, checkDaemonOrphan, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMofloYamlCompliance, checkStatusLine, checkTestDirs, } from './doctor-checks-config.js';
|
|
15
|
+
import { checkConfigFile, checkDaemonIdentity, checkDaemonOrphan, checkDaemonStatus, checkDaemonWriteRouting, checkMcpServers, checkMemoryDatabase, checkMemoryDbIntegrity, checkMofloYamlCompliance, checkStatusLine, checkSwarmResidue, checkTestDirs, } from './doctor-checks-config.js';
|
|
16
16
|
import { checkSpellEngine, checkSandboxTier } from './doctor-checks-platform.js';
|
|
17
17
|
import { checkEmbeddings, checkSemanticQuality, } from './doctor-checks-memory.js';
|
|
18
18
|
import { checkIntelligence } from './doctor-checks-intelligence.js';
|
|
@@ -42,6 +42,10 @@ export const allChecks = [
|
|
|
42
42
|
checkDaemonWriteRouting,
|
|
43
43
|
checkWritersAudit,
|
|
44
44
|
checkMemoryDatabase,
|
|
45
|
+
// Surfaces leftover `.swarm/` artifacts (memory.db, router state, logs) so
|
|
46
|
+
// the auto-fix can relocate or delete them. Independent of the canonical
|
|
47
|
+
// DB checks — runs cheap (statSync only).
|
|
48
|
+
checkSwarmResidue,
|
|
45
49
|
// Owns the corruption signal so downstream checks (Embeddings, Semantic
|
|
46
50
|
// Quality, Memory Access Functional) don't surface it as the synthetic
|
|
47
51
|
// "Check" failure (doctor.ts:214). MUST run after checkMemoryDatabase
|
|
@@ -107,6 +111,8 @@ export const componentMap = {
|
|
|
107
111
|
'writers-audit': checkWritersAudit,
|
|
108
112
|
'writers': checkWritersAudit,
|
|
109
113
|
'memory': checkMemoryDatabase,
|
|
114
|
+
'swarm-residue': checkSwarmResidue,
|
|
115
|
+
'residue': checkSwarmResidue,
|
|
110
116
|
'memory-db-integrity': checkMemoryDbIntegrity,
|
|
111
117
|
'integrity': checkMemoryDbIntegrity,
|
|
112
118
|
'memory-integrity': checkMemoryDbIntegrity,
|
|
@@ -80,7 +80,7 @@ const DEFAULT_CONFIG = {
|
|
|
80
80
|
maxUncertainty: 0.15,
|
|
81
81
|
enableCircuitBreaker: true,
|
|
82
82
|
circuitBreakerThreshold: 5,
|
|
83
|
-
statePath: '.
|
|
83
|
+
statePath: '.moflo/movector/model-router-state.json',
|
|
84
84
|
autoSaveInterval: 1, // Save after every decision for CLI persistence
|
|
85
85
|
enableCostOptimization: true,
|
|
86
86
|
preferSpeed: true,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Optimized state space with feature hashing
|
|
10
10
|
* - Epsilon decay with exponential annealing
|
|
11
11
|
* - Experience replay buffer for stable learning
|
|
12
|
-
* - Model persistence to .
|
|
12
|
+
* - Model persistence to .moflo/movector/q-learning-model.json
|
|
13
13
|
*
|
|
14
14
|
* @module q-learning-router
|
|
15
15
|
*/
|
|
@@ -32,7 +32,7 @@ const DEFAULT_CONFIG = {
|
|
|
32
32
|
enableReplay: true,
|
|
33
33
|
cacheSize: 256,
|
|
34
34
|
cacheTTL: 300000,
|
|
35
|
-
modelPath: '.
|
|
35
|
+
modelPath: '.moflo/movector/q-learning-model.json',
|
|
36
36
|
autoSaveInterval: 100,
|
|
37
37
|
stateSpaceDim: 64,
|
|
38
38
|
};
|
|
@@ -798,6 +798,14 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
798
798
|
.btn-primary { background: #238636; border-color: #2ea043; color: #fff; }
|
|
799
799
|
.btn-primary:hover { background: #2ea043; border-color: #3fb950; }
|
|
800
800
|
.dim { color: #484f58; font-size: 0.75rem; font-style: italic; }
|
|
801
|
+
/* Loading state for tabs whose data is slow on first paint (currently
|
|
802
|
+
Claude Stats, which walks the user's transcript dir — can take 10–15s
|
|
803
|
+
on a long history). Pure-CSS spinner; no image, no framework. */
|
|
804
|
+
.loading-block { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; padding: 48px 16px; color: #8b949e; }
|
|
805
|
+
.loading-block .spinner { width: 28px; height: 28px; border: 3px solid #30363d; border-top-color: #58a6ff; border-radius: 50%; animation: lum-spin 0.85s linear infinite; }
|
|
806
|
+
.loading-block .msg { font-size: 0.9rem; color: #c9d1d9; }
|
|
807
|
+
.loading-block .hint { font-size: 0.8rem; color: #8b949e; font-style: italic; max-width: 480px; text-align: center; line-height: 1.5; }
|
|
808
|
+
@keyframes lum-spin { to { transform: rotate(360deg); } }
|
|
801
809
|
</style>
|
|
802
810
|
</head>
|
|
803
811
|
<body>
|
|
@@ -811,7 +819,7 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
811
819
|
<div id="panel-schedules" class="panel" style="display:none"><div id="schedules-active"></div><div id="schedules-events"></div></div>
|
|
812
820
|
<div id="panel-executions" class="panel" style="display:none"></div>
|
|
813
821
|
<div id="panel-memory" class="panel" style="display:none"></div>
|
|
814
|
-
<div id="panel-claude-stats" class="panel" style="display:none"></div>
|
|
822
|
+
<div id="panel-claude-stats" class="panel" style="display:none"><div class="loading-block" role="status" aria-label="Loading Claude Code transcripts"><div class="spinner"></div><div class="msg">Reading Claude Code transcripts…</div><div class="hint">First load can take 10–15 seconds — moflo walks every session file in this project's transcript directory. Subsequent loads in this tab are much faster.</div></div></div>
|
|
815
823
|
<div id="poll-indicator" class="poll-indicator"></div>
|
|
816
824
|
<script>
|
|
817
825
|
// Tab navigation — plain DOM, no framework
|
|
@@ -1139,7 +1147,19 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
|
|
|
1139
1147
|
};
|
|
1140
1148
|
function renderClaudeStats(cs) {
|
|
1141
1149
|
const el = document.getElementById('panel-claude-stats');
|
|
1142
|
-
|
|
1150
|
+
// cs is null on first paint AND on fetch error (Promise chain uses
|
|
1151
|
+
// .catch(() => null)). Render the spinner block on both so the user
|
|
1152
|
+
// sees motion during the 10–15s transcript walk and during a transient
|
|
1153
|
+
// network blip — better than a static "Loading..." that looks frozen.
|
|
1154
|
+
if (!cs) {
|
|
1155
|
+
el.innerHTML =
|
|
1156
|
+
'<div class="loading-block" role="status" aria-label="Loading Claude Code transcripts">' +
|
|
1157
|
+
'<div class="spinner"></div>' +
|
|
1158
|
+
'<div class="msg">Reading Claude Code transcripts…</div>' +
|
|
1159
|
+
'<div class="hint">First load can take 10–15 seconds — moflo walks every session file in this project\\'s transcript directory. Subsequent loads in this tab are much faster.</div>' +
|
|
1160
|
+
'</div>';
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1143
1163
|
|
|
1144
1164
|
// Always-visible disclaimer banner — keeps the scope and limits in
|
|
1145
1165
|
// view so the numbers aren't read as account-wide truth.
|
|
@@ -214,4 +214,39 @@ export function probeDaemonHealth(port, timeoutMs) {
|
|
|
214
214
|
req.on('timeout', () => { req.destroy(); finish({ kind: 'unreachable' }); });
|
|
215
215
|
});
|
|
216
216
|
}
|
|
217
|
+
/** Retry backoff schedule for HTTP liveness probes (#1163 — Windows CI race). */
|
|
218
|
+
const PROBE_RETRY_BACKOFF_MS = [50, 200, 800];
|
|
219
|
+
/**
|
|
220
|
+
* {@link probeDaemonHealth} with retry on transient `unreachable` results.
|
|
221
|
+
*
|
|
222
|
+
* The bare one-shot probe was tripping in CI when a daemon was mid-boot on
|
|
223
|
+
* Windows: the lockfile said v4.10.8 was alive but the HTTP server hadn't
|
|
224
|
+
* accepted /api/health yet, and the doctor check raised "unreachable" inside
|
|
225
|
+
* the 1500ms window. This wrapper retries 3× at 50/200/800 ms — total
|
|
226
|
+
* worst-case ~1s extra — and only on `unreachable`. `identity` and `legacy`
|
|
227
|
+
* are terminal: a daemon answering with a different project root or 404 is
|
|
228
|
+
* a real signal, not a race.
|
|
229
|
+
*
|
|
230
|
+
* Mirrors the retry pattern from `bin/lib/file-sync.mjs:syncWithRetry`
|
|
231
|
+
* (`feedback_transient_retry_circuit_breaker` — every transient-error op
|
|
232
|
+
* uses 50/200/800ms backoff).
|
|
233
|
+
*
|
|
234
|
+
* Worst-case elapsed = (PROBE_RETRY_BACKOFF_MS.length + 1) × timeoutMs
|
|
235
|
+
* + sum(PROBE_RETRY_BACKOFF_MS). For doctor's 1500ms timeout that's
|
|
236
|
+
* 4 × 1500 + 1050 ≈ 7s, fully off the hot path; callers picking a tighter
|
|
237
|
+
* timeout should account for the 4× multiplier.
|
|
238
|
+
*/
|
|
239
|
+
export async function probeDaemonHealthWithRetry(port, timeoutMs) {
|
|
240
|
+
let last = { kind: 'unreachable' };
|
|
241
|
+
const maxAttempts = PROBE_RETRY_BACKOFF_MS.length + 1;
|
|
242
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
243
|
+
if (attempt > 0) {
|
|
244
|
+
await new Promise((resolve) => setTimeout(resolve, PROBE_RETRY_BACKOFF_MS[attempt - 1]));
|
|
245
|
+
}
|
|
246
|
+
last = await probeDaemonHealth(port, timeoutMs);
|
|
247
|
+
if (last.kind !== 'unreachable')
|
|
248
|
+
return last;
|
|
249
|
+
}
|
|
250
|
+
return last;
|
|
251
|
+
}
|
|
217
252
|
//# sourceMappingURL=daemon-port.js.map
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.10.
|
|
3
|
+
"version": "4.10.10",
|
|
4
4
|
"description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
|
|
5
5
|
"main": "dist/src/cli/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
96
96
|
"@typescript-eslint/parser": "^7.18.0",
|
|
97
97
|
"eslint": "^8.0.0",
|
|
98
|
-
"moflo": "^4.10.
|
|
98
|
+
"moflo": "^4.10.9",
|
|
99
99
|
"tsx": "^4.21.0",
|
|
100
100
|
"typescript": "^5.9.3",
|
|
101
101
|
"vitest": "^4.0.0"
|