moflo 4.10.2 → 4.10.3
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/bin/session-start-launcher.mjs +112 -5
- package/dist/src/cli/commands/doctor-checks-config.js +4 -4
- package/dist/src/cli/commands/doctor-version.js +1 -1
- package/dist/src/cli/commands/index.js +0 -6
- package/dist/src/cli/init/executor.js +2 -2
- package/dist/src/cli/mcp-tools/swarm-tools.js +3 -4
- package/dist/src/cli/services/moflo-paths.js +6 -5
- package/dist/src/cli/services/moflo-require.js +2 -2
- package/dist/src/cli/shared/core/config/loader.js +2 -2
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/dist/src/cli/appliance/gguf-engine.js +0 -425
- package/dist/src/cli/appliance/ruvllm-bridge.js +0 -231
- package/dist/src/cli/appliance/rvfa-builder.js +0 -325
- package/dist/src/cli/appliance/rvfa-distribution.js +0 -370
- package/dist/src/cli/appliance/rvfa-format.js +0 -393
- package/dist/src/cli/appliance/rvfa-runner.js +0 -238
- package/dist/src/cli/appliance/rvfa-signing.js +0 -351
- package/dist/src/cli/commands/appliance-advanced.js +0 -213
- package/dist/src/cli/commands/appliance.js +0 -404
|
@@ -358,10 +358,67 @@ function fireAndForget(cmd, args, label) {
|
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
// Cross-platform sync sleep — Atomics.wait parks the thread at the OS level
|
|
362
|
+
// without burning CPU (same primitive as src/cli/shared/utils/atomic-file-
|
|
363
|
+
// write.ts:131). Used by stopDaemon's liveness polling between graceful and
|
|
364
|
+
// forced termination so we never unlink the lockfile while the daemon is
|
|
365
|
+
// still running.
|
|
366
|
+
const STOP_SLEEP_BUF = new Int32Array(new SharedArrayBuffer(4));
|
|
367
|
+
function sleepSyncMs(ms) {
|
|
368
|
+
Atomics.wait(STOP_SLEEP_BUF, 0, 0, ms);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// PID liveness check. EPERM means the process exists but is owned by another
|
|
372
|
+
// user — treat as alive (matches the canonical isAlive in process-manager.mjs
|
|
373
|
+
// after #1061; the prior `catch { return false; }` falsely reported foreign-
|
|
374
|
+
// owned daemons as dead and let the lockfile be unlinked under them).
|
|
375
|
+
//
|
|
376
|
+
// Linux zombie handling: on Linux, `kill(pid, 0)` returns success for zombie
|
|
377
|
+
// processes (exited but not yet reaped by their parent). A zombie can't write
|
|
378
|
+
// to the DB, hold locks, or do anything else stopDaemon cares about — treating
|
|
379
|
+
// it as alive exhausts the kill budget polling a corpse, then preserves the
|
|
380
|
+
// lockfile under a dead process. Production never hits this (the daemon is
|
|
381
|
+
// detached and reaped by init/systemd within ~ms), but a misbehaving parent
|
|
382
|
+
// can keep a daemon zombified, and the launcher's vitest harness reproduces
|
|
383
|
+
// the case deterministically (#1083 CI failure on ubuntu-latest). Read
|
|
384
|
+
// /proc/<pid>/stat (fixed-format, cheap) and treat 'Z' as dead.
|
|
385
|
+
function isDaemonPidAlive(pid) {
|
|
386
|
+
try {
|
|
387
|
+
process.kill(pid, 0);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
return err && err.code === 'EPERM';
|
|
390
|
+
}
|
|
391
|
+
if (process.platform === 'linux') {
|
|
392
|
+
try {
|
|
393
|
+
const stat = readFileSync(`/proc/${pid}/stat`, 'utf-8');
|
|
394
|
+
// Format: "pid (comm) state ..." — comm can contain spaces/parens, so
|
|
395
|
+
// parse from the LAST ')' to skip it safely.
|
|
396
|
+
const lastParen = stat.lastIndexOf(')');
|
|
397
|
+
if (lastParen !== -1 && stat.charAt(lastParen + 2) === 'Z') return false;
|
|
398
|
+
} catch (err) {
|
|
399
|
+
// ENOENT = pid vanished between kill(0) and the read — already dead.
|
|
400
|
+
if (err && err.code === 'ENOENT') return false;
|
|
401
|
+
// Anything else (e.g. /proc unavailable) — keep the kill(0) verdict.
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
|
|
361
407
|
// Stop the daemon recorded in `lockFile` (if any) without restarting. Used by
|
|
362
408
|
// the upgrade flow before any DB work — the daemon must not be holding old
|
|
363
409
|
// path resolution in memory, and a concurrent sql.js flush would clobber the
|
|
364
|
-
// cherry-picked rows. Returns true when a live PID was
|
|
410
|
+
// cherry-picked rows. Returns true when a live PID was confirmed dead (or the
|
|
411
|
+
// PID was already gone when we read the lockfile).
|
|
412
|
+
//
|
|
413
|
+
// Escalation mirrors src/cli/commands/daemon.ts:killBackgroundDaemon so the
|
|
414
|
+
// launcher's upgrade path and `flo daemon stop` behave identically: graceful
|
|
415
|
+
// signal → wait → liveness check → force kill. The prior implementation sent
|
|
416
|
+
// bare `process.kill(pid, 'SIGTERM')` on every platform, which on Windows
|
|
417
|
+
// either silently force-kills or fails entirely depending on the process; in
|
|
418
|
+
// either case the catch swallowed the outcome and the lockfile was unlinked.
|
|
419
|
+
// The daemon (if it survived) then re-wrote the lockfile with its stale PID +
|
|
420
|
+
// pre-upgrade version, defeating the section-3a-pre version-skew recovery and
|
|
421
|
+
// leaving the statusline stuck on `📊 ?` until manual `flo daemon restart`.
|
|
365
422
|
//
|
|
366
423
|
// Section 4's `hooks.mjs session-start` spawn is responsible for starting a
|
|
367
424
|
// fresh daemon under the current code; this function intentionally does not.
|
|
@@ -372,11 +429,61 @@ function stopDaemon(lockFile) {
|
|
|
372
429
|
const lock = JSON.parse(readFileSync(lockFile, 'utf-8'));
|
|
373
430
|
if (typeof lock?.pid === 'number' && lock.pid > 0) stalePid = lock.pid;
|
|
374
431
|
} catch { /* malformed lock — fall through to unlink */ }
|
|
375
|
-
|
|
376
|
-
|
|
432
|
+
|
|
433
|
+
let killed = false;
|
|
434
|
+
if (stalePid !== null && isDaemonPidAlive(stalePid)) {
|
|
435
|
+
// Graceful signal — platform-aware. On Windows, `process.kill(pid, 'SIGTERM')`
|
|
436
|
+
// silently force-kills (skipping the daemon's shutdown handlers that flush
|
|
437
|
+
// sql.js + release lock cleanly), so use bare `taskkill` (no /F) for a
|
|
438
|
+
// close-event signal.
|
|
439
|
+
try {
|
|
440
|
+
if (process.platform === 'win32') {
|
|
441
|
+
execFileSync('taskkill', ['/PID', String(stalePid)], { windowsHide: true, timeout: 5000 });
|
|
442
|
+
} else {
|
|
443
|
+
process.kill(stalePid, 'SIGTERM');
|
|
444
|
+
}
|
|
445
|
+
} catch { /* signal/spawn failed — fall through to liveness poll + force */ }
|
|
446
|
+
|
|
447
|
+
// Poll for death up to 3s. The daemon's shutdown handler does a final
|
|
448
|
+
// sql.js dump + lock release, which under load can take ~1s.
|
|
449
|
+
const gracefulDeadline = Date.now() + 3000;
|
|
450
|
+
while (Date.now() < gracefulDeadline) {
|
|
451
|
+
if (!isDaemonPidAlive(stalePid)) { killed = true; break; }
|
|
452
|
+
sleepSyncMs(100);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Force-kill if still alive.
|
|
456
|
+
if (!killed) {
|
|
457
|
+
try {
|
|
458
|
+
if (process.platform === 'win32') {
|
|
459
|
+
execFileSync('taskkill', ['/F', '/T', '/PID', String(stalePid)], { windowsHide: true, timeout: 5000 });
|
|
460
|
+
} else {
|
|
461
|
+
process.kill(stalePid, 'SIGKILL');
|
|
462
|
+
}
|
|
463
|
+
} catch { /* dead or unreachable */ }
|
|
464
|
+
// Short grace period for OS reap.
|
|
465
|
+
const forceDeadline = Date.now() + 1000;
|
|
466
|
+
while (Date.now() < forceDeadline) {
|
|
467
|
+
if (!isDaemonPidAlive(stalePid)) { killed = true; break; }
|
|
468
|
+
sleepSyncMs(100);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!killed) {
|
|
473
|
+
// Daemon survived both signals. Leave the lockfile in place so the next
|
|
474
|
+
// session can see the stale PID and retry — unlinking now would let the
|
|
475
|
+
// surviving daemon re-write the lockfile with its stale PID + version,
|
|
476
|
+
// perpetuating the loop this fix exists to break.
|
|
477
|
+
emitWarning(`stopDaemon: PID ${stalePid} did not exit after SIGTERM+force-kill; lockfile preserved`);
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
} else if (stalePid !== null) {
|
|
481
|
+
// PID was in the lockfile but the process is already gone — clean unlink.
|
|
482
|
+
killed = true;
|
|
377
483
|
}
|
|
484
|
+
|
|
378
485
|
try { unlinkSync(lockFile); } catch { /* non-fatal */ }
|
|
379
|
-
return
|
|
486
|
+
return killed;
|
|
380
487
|
}
|
|
381
488
|
|
|
382
489
|
// Stop-and-restart helper for the stale-daemon branch (section 3a-pre). The
|
|
@@ -720,7 +827,7 @@ try {
|
|
|
720
827
|
|
|
721
828
|
// ── Sync .claude/agents/ + .claude/skills/ recursively (#948) ──────
|
|
722
829
|
// Pre-#948, agents and skills weren't manifest-tracked at all, so any
|
|
723
|
-
// file moflo retired (e.g. the 49
|
|
830
|
+
// file moflo retired (e.g. the 49 aspirational agents in #932 or
|
|
724
831
|
// skill-builder in #945) would linger forever in consumer projects —
|
|
725
832
|
// Claude Code kept loading them on every prompt, paying the per-prompt
|
|
726
833
|
// roster tokens we just spent #932 fixing. Walking these dirs through
|
|
@@ -13,7 +13,7 @@ import { errorDetail } from '../shared/utils/error-detail.js';
|
|
|
13
13
|
export async function checkConfigFile() {
|
|
14
14
|
// JSON configs (parse-validated). LEGACY-CONFIG: `.claude-flow.json` and
|
|
15
15
|
// `claude-flow.config.json` filenames are still recognised so consumers
|
|
16
|
-
// upgrading from pre-#699 moflo builds
|
|
16
|
+
// upgrading from pre-#699 moflo builds keep working
|
|
17
17
|
// without manual rename. Drift guard exempts these via LEGACY-CONFIG marker.
|
|
18
18
|
const jsonPaths = [
|
|
19
19
|
'.moflo/config.json',
|
|
@@ -233,18 +233,18 @@ export async function checkMcpServers() {
|
|
|
233
233
|
const content = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
234
234
|
const servers = content.mcpServers || content.servers || {};
|
|
235
235
|
const count = Object.keys(servers).length;
|
|
236
|
-
const hasClaudeFlow = 'moflo' in servers || 'claude-flow' in servers || 'claude-flow_alpha' in servers
|
|
236
|
+
const hasClaudeFlow = 'moflo' in servers || 'claude-flow' in servers || 'claude-flow_alpha' in servers;
|
|
237
237
|
if (hasClaudeFlow) {
|
|
238
238
|
return { name: 'MCP Servers', status: 'pass', message: `${count} servers (flo configured)` };
|
|
239
239
|
}
|
|
240
|
-
return { name: 'MCP Servers', status: 'warn', message: `${count} servers (flo not found)`, fix: 'claude mcp add
|
|
240
|
+
return { name: 'MCP Servers', status: 'warn', message: `${count} servers (flo not found)`, fix: 'claude mcp add moflo -- npx -y moflo mcp start' };
|
|
241
241
|
}
|
|
242
242
|
catch {
|
|
243
243
|
// continue to next path
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
|
-
return { name: 'MCP Servers', status: 'warn', message: 'No MCP config found', fix: 'claude mcp add moflo npx moflo mcp start' };
|
|
247
|
+
return { name: 'MCP Servers', status: 'warn', message: 'No MCP config found', fix: 'claude mcp add moflo -- npx -y moflo mcp start' };
|
|
248
248
|
}
|
|
249
249
|
// Catches three failure modes (#895):
|
|
250
250
|
// 1. File missing — session-start should have created it; warn user that
|
|
@@ -27,7 +27,7 @@ function readCurrentVersion() {
|
|
|
27
27
|
const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
28
28
|
if (pkg.version &&
|
|
29
29
|
typeof pkg.name === 'string' &&
|
|
30
|
-
(pkg.name === 'moflo' || pkg.name === 'claude-flow'
|
|
30
|
+
(pkg.name === 'moflo' || pkg.name === 'claude-flow')) {
|
|
31
31
|
return pkg.version;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -62,8 +62,6 @@ const commandLoaders = {
|
|
|
62
62
|
benchmark: () => import('./benchmark.js'),
|
|
63
63
|
// Guidance Control Plane
|
|
64
64
|
guidance: () => import('./guidance.js'),
|
|
65
|
-
// RVFA Appliance Management
|
|
66
|
-
appliance: () => import('./appliance.js'),
|
|
67
65
|
// MoFlo Spell Gates
|
|
68
66
|
gate: () => import('./gate.js'),
|
|
69
67
|
// Feature Orchestrator
|
|
@@ -137,7 +135,6 @@ import { issuesCommand } from './issues.js';
|
|
|
137
135
|
import updateCommand from './update.js';
|
|
138
136
|
import { processCommand } from './process.js';
|
|
139
137
|
import { guidanceCommand } from './guidance.js';
|
|
140
|
-
import { applianceCommand } from './appliance.js';
|
|
141
138
|
import { diagnoseCommand } from './diagnose.js';
|
|
142
139
|
import { githubCommand } from './github.js';
|
|
143
140
|
// Pre-populate cache with core commands
|
|
@@ -184,7 +181,6 @@ export { performanceCommand } from './performance.js';
|
|
|
184
181
|
export { securityCommand } from './security.js';
|
|
185
182
|
export { hiveMindCommand } from './hive-mind.js';
|
|
186
183
|
export { guidanceCommand } from './guidance.js';
|
|
187
|
-
export { applianceCommand } from './appliance.js';
|
|
188
184
|
export { diagnoseCommand } from './diagnose.js';
|
|
189
185
|
export { githubCommand } from './github.js';
|
|
190
186
|
// Lazy-loaded command re-exports (for backwards compatibility, but async-only)
|
|
@@ -209,7 +205,6 @@ export async function getRouteCommand() { return loadCommand('route'); }
|
|
|
209
205
|
export async function getProgressCommand() { return loadCommand('progress'); }
|
|
210
206
|
export async function getIssuesCommand() { return loadCommand('issues'); }
|
|
211
207
|
export async function getGuidanceCommand() { return loadCommand('guidance'); }
|
|
212
|
-
export async function getApplianceCommand() { return loadCommand('appliance'); }
|
|
213
208
|
/**
|
|
214
209
|
* Core commands loaded synchronously (available immediately)
|
|
215
210
|
* Advanced commands loaded on-demand for faster startup
|
|
@@ -285,7 +280,6 @@ export const commandsByCategory = {
|
|
|
285
280
|
issuesCommand,
|
|
286
281
|
updateCommand,
|
|
287
282
|
processCommand,
|
|
288
|
-
applianceCommand,
|
|
289
283
|
githubCommand,
|
|
290
284
|
],
|
|
291
285
|
};
|
|
@@ -70,8 +70,8 @@ const COMMANDS_MAP = {};
|
|
|
70
70
|
* Agents to copy based on configuration. Exported for integrity tests.
|
|
71
71
|
*
|
|
72
72
|
* Each value is a directory name under `.claude/agents/` that ships in the
|
|
73
|
-
* moflo package. After #932 retired ~50
|
|
74
|
-
*
|
|
73
|
+
* moflo package. After #932 retired ~50 aspirational agents, the set is
|
|
74
|
+
* narrowed to actual development specialties Claude is likely to invoke.
|
|
75
75
|
*/
|
|
76
76
|
export const AGENTS_MAP = {
|
|
77
77
|
core: ['core'],
|
|
@@ -12,7 +12,7 @@ import { getSwarmCoordinator, isSwarmCoordinatorInitialized, } from './swarm-coo
|
|
|
12
12
|
import { scaleHandler, SCALE_STRATEGIES, TARGET_AGENTS_MIN, TARGET_AGENTS_MAX, } from './swarm-scale-handler.js';
|
|
13
13
|
import { findProjectRoot } from '../services/project-root.js';
|
|
14
14
|
import { MOFLO_DIR } from '../services/moflo-paths.js';
|
|
15
|
-
// Inputs accepted by the MCP layer (covers
|
|
15
|
+
// Inputs accepted by the MCP layer (covers legacy aliases). The coordinator's
|
|
16
16
|
// TopologyType is narrower: 'mesh' | 'hierarchical' | 'centralized' | 'hybrid'.
|
|
17
17
|
const TOPOLOGY_MAP = {
|
|
18
18
|
hierarchical: 'hierarchical',
|
|
@@ -23,9 +23,8 @@ const TOPOLOGY_MAP = {
|
|
|
23
23
|
'hierarchical-mesh': 'hybrid',
|
|
24
24
|
hybrid: 'hybrid',
|
|
25
25
|
};
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// `byzantine`/`raft`/`gossip`/`paxos`.
|
|
26
|
+
// `unanimous`/`weighted`/`majority` are the user-facing aliases; the
|
|
27
|
+
// coordinator only speaks `byzantine`/`raft`/`gossip`/`paxos`.
|
|
29
28
|
const CONSENSUS_MAP = {
|
|
30
29
|
unanimous: { algorithm: 'byzantine', threshold: 1.0 },
|
|
31
30
|
byzantine: { algorithm: 'byzantine', threshold: 1.0 },
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MoFlo runtime state directory constants.
|
|
3
3
|
*
|
|
4
|
-
* MoFlo owns its state under `.moflo/` at the project root.
|
|
5
|
-
*
|
|
4
|
+
* MoFlo owns its state under `.moflo/` at the project root. Pre-#699 builds
|
|
5
|
+
* used `.claude-flow/`; both legacy locations are still recognized as
|
|
6
6
|
* read-only sources for the version-bump-gated cherry-pick (#851) but are
|
|
7
7
|
* never relocated or renamed automatically — leaving them in place gives
|
|
8
8
|
* consumers a recovery source and avoids the failure modes that motivated
|
|
@@ -27,11 +27,12 @@ export const MEMORY_DB_FILE = 'moflo.db';
|
|
|
27
27
|
/** HNSW persisted index sidecar. Lives next to the DB at `<root>/.moflo/hnsw.index`. */
|
|
28
28
|
export const HNSW_INDEX_FILE = 'hnsw.index';
|
|
29
29
|
/**
|
|
30
|
-
* Legacy runtime directory
|
|
31
|
-
* migration code paths — production code should use
|
|
30
|
+
* Legacy `.claude-flow/` runtime directory used by pre-#699 moflo builds.
|
|
31
|
+
* Only referenced from migration code paths — production code should use
|
|
32
|
+
* {@link MOFLO_DIR}.
|
|
32
33
|
*/
|
|
33
34
|
export const LEGACY_CLAUDE_FLOW_DIR = '.claude-flow';
|
|
34
|
-
/** Legacy `.swarm/` directory used by
|
|
35
|
+
/** Legacy `.swarm/` directory used by pre-#727 moflo builds for the memory DB. */
|
|
35
36
|
export const LEGACY_SWARM_DIR = '.swarm';
|
|
36
37
|
/** Legacy memory DB filename — only ever inside `.swarm/`. Pre-#727. */
|
|
37
38
|
export const LEGACY_MEMORY_DB_FILE = 'memory.db';
|
|
@@ -116,8 +116,8 @@ export function mofloResolve(specifier) {
|
|
|
116
116
|
// ≈ 6 hops to the moflo root. 12 gives headroom for worktree/monorepo layouts.
|
|
117
117
|
const MAX_WALK_DEPTH = 12;
|
|
118
118
|
// Names a package.json may carry while still being "us" — covers the moflo
|
|
119
|
-
// rename
|
|
120
|
-
const MOFLO_PACKAGE_NAMES = new Set(['moflo', 'claude-flow'
|
|
119
|
+
// rename from the pre-collapse claude-flow workspace.
|
|
120
|
+
const MOFLO_PACKAGE_NAMES = new Set(['moflo', 'claude-flow']);
|
|
121
121
|
// Walk up from this file's dir, returning the first non-null `test(dir)` result.
|
|
122
122
|
function walkUpFromSelf(test) {
|
|
123
123
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -11,8 +11,8 @@ import { defaultSystemConfig, mergeWithDefaults } from './defaults.js';
|
|
|
11
11
|
/**
|
|
12
12
|
* Configuration file names to search for. Canonical names come first;
|
|
13
13
|
* `claude-flow.*` names are kept as legacy fallback so consumers upgrading
|
|
14
|
-
* from
|
|
15
|
-
*
|
|
14
|
+
* from pre-#699 moflo builds keep loading their existing config without a
|
|
15
|
+
* manual rename.
|
|
16
16
|
*/
|
|
17
17
|
const CONFIG_FILE_NAMES = [
|
|
18
18
|
'moflo.config.json',
|
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.3",
|
|
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.2",
|
|
99
99
|
"tsx": "^4.21.0",
|
|
100
100
|
"typescript": "^5.9.3",
|
|
101
101
|
"vitest": "^4.0.0"
|