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.
@@ -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 actually killed.
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
- if (stalePid !== null) {
376
- try { process.kill(stalePid, 'SIGTERM'); } catch { /* already dead */ }
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 stalePid !== null;
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 ruflo-aspirational agents in #932 or
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 (upstream Ruflo) keep working
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 || 'ruflo' in servers || 'ruflo_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 ruflo -- npx -y ruflo@latest mcp start' };
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' || pkg.name === 'ruflo')) {
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 ruflo-aspirational agents, the set
74
- * is narrowed to actual development specialties Claude is likely to invoke.
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 Ruflo aliases). The coordinator's
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
- // Ported from Ruflo v3/mcp/tools/swarm-tools.ts. `unanimous`/`weighted`/
27
- // `majority` are the user-facing aliases; the coordinator only speaks
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. The upstream Ruflo
5
- * fork used `.claude-flow/`; both legacy locations are still recognized as
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 inherited from upstream Ruflo. Only referenced from
31
- * migration code paths — production code should use {@link MOFLO_DIR}.
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 Ruflo + pre-#727 moflo for the memory DB. */
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 and the upstream forks we tolerate during version migration.
120
- const MOFLO_PACKAGE_NAMES = new Set(['moflo', 'claude-flow', 'ruflo']);
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 older moflo builds (which inherited the upstream Ruflo filenames)
15
- * keep loading their existing config without a manual rename.
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',
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.10.2';
5
+ export const VERSION = '4.10.3';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.10.2",
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.1",
98
+ "moflo": "^4.10.2",
99
99
  "tsx": "^4.21.0",
100
100
  "typescript": "^5.9.3",
101
101
  "vitest": "^4.0.0"