moflo 4.10.1 → 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.
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { output } from '../output.js';
14
14
  import { allChecks, componentMap, zombieScanCheck } from './doctor-registry.js';
15
- import { emitJsonOutput, finalize, formatCheck, maybeAutoInstallClaudeCode, renderSummary, runAutoFix, runKillZombiesBanner, } from './doctor-render.js';
15
+ import { emitJsonOutput, finalize, formatCheck, maybeAutoInstallClaudeCode, renderSummary, runAutoFix, runKillZombies, } from './doctor-render.js';
16
16
  import { checkEmbeddings } from './doctor-checks-memory.js';
17
17
  import { checkMofloYamlCompliance } from './doctor-checks-config.js';
18
18
  // Re-export for tests + external consumers (#639 stale-vector-stats test
@@ -125,24 +125,21 @@ export const doctorCommand = {
125
125
  output.writeln(output.warning('--allow-warn requires --strict; ignoring (warnings are tolerated by default).'));
126
126
  output.writeln();
127
127
  }
128
- if (killZombies) {
129
- await runKillZombiesBanner();
130
- }
131
128
  const checksToRun = component && componentMap[component]
132
129
  ? [componentMap[component]]
133
130
  : allChecks;
134
131
  const results = [];
135
132
  const fixes = [];
136
- // OPTIMIZATION: Run all checks in parallel for 3-5x faster execution
137
- const spinner = jsonOutput
138
- ? null
139
- : output.createSpinner({ text: 'Running health checks in parallel...', spinner: 'dots' });
140
- spinner?.start();
133
+ let zombieScan;
134
+ let claudeCodeInstall;
135
+ let fixesApplied;
141
136
  // Issue #818: in --json mode, several deep checks (spell engine probe,
142
137
  // mcp-spell bridge, etc.) write `[spell] ...` log lines straight to
143
138
  // stdout — that breaks the single-JSON-document contract. Capture and
144
- // discard stdout writes while checks run; restore in `finally` so a
145
- // throw can't leave the process with a stubbed stdout.
139
+ // discard stdout writes while checks AND post-check actions run; restore
140
+ // in `finally` so a throw can't leave the process with a stubbed stdout.
141
+ // Issue #1122: extended to wrap zombie-kill banner, --install, and
142
+ // --fix work so each runs on the JSON path with prose suppressed.
146
143
  const realStdoutWrite = process.stdout.write.bind(process.stdout);
147
144
  const restoreStdout = () => {
148
145
  if (jsonOutput) {
@@ -153,7 +150,18 @@ export const doctorCommand = {
153
150
  process.stdout.write =
154
151
  (..._args) => true;
155
152
  }
153
+ // OPTIMIZATION: Run all checks in parallel for 3-5x faster execution
154
+ const spinner = jsonOutput
155
+ ? null
156
+ : output.createSpinner({ text: 'Running health checks in parallel...', spinner: 'dots' });
156
157
  try {
158
+ // Issue #1122: kill-zombies prose used to write BEFORE the JSON
159
+ // suppression activated, corrupting the JSON document. Now runs
160
+ // under suppression and feeds a structured result into the payload.
161
+ if (killZombies) {
162
+ zombieScan = await runKillZombies({ silent: jsonOutput });
163
+ }
164
+ spinner?.start();
157
165
  let checkResults;
158
166
  try {
159
167
  checkResults = await Promise.allSettled(checksToRun.map(check => check()));
@@ -174,7 +182,6 @@ export const doctorCommand = {
174
182
  }
175
183
  finally {
176
184
  spinner?.stop();
177
- restoreStdout();
178
185
  }
179
186
  for (const settledResult of checkResults) {
180
187
  if (settledResult.status === 'fulfilled') {
@@ -197,26 +204,64 @@ export const doctorCommand = {
197
204
  output.writeln(formatCheck(errorResult));
198
205
  }
199
206
  }
207
+ // Issue #1122: action flags must run on BOTH the JSON path and the
208
+ // formatted path. Previously the JSON branch early-returned before
209
+ // any of this ran, so `--json --fix` (and `--json --install`) silently
210
+ // no-op'd. Now they execute under stdout suppression and their
211
+ // outcomes feed the JSON payload below.
212
+ if (autoInstall) {
213
+ claudeCodeInstall = await maybeAutoInstallClaudeCode(results, fixes, { silent: jsonOutput });
214
+ }
215
+ if (!jsonOutput)
216
+ renderSummary(results);
217
+ if (showFix && fixes.length > 0) {
218
+ const outcome = await runAutoFix(results, fixes, checksToRun, { silent: jsonOutput });
219
+ fixesApplied = outcome.fixesApplied;
220
+ // Replace `results` with post-fix state so JSON consumers see the
221
+ // re-evaluated truth, not the pre-fix snapshot. Mirror the #992
222
+ // post-parallel zombie-scan append so the post-fix shape matches
223
+ // pre-fix shape (otherwise `--json --fix` silently drops the
224
+ // Zombie Processes entry from the JSON `results[]`).
225
+ if (outcome.reEvaluated) {
226
+ const finalChecks = [...outcome.reEvaluated];
227
+ if (!component) {
228
+ try {
229
+ finalChecks.push(await zombieScanCheck());
230
+ }
231
+ catch (reason) {
232
+ finalChecks.push({
233
+ name: 'Zombie Processes',
234
+ status: 'fail',
235
+ message: reason?.message ?? 'Unknown error',
236
+ });
237
+ }
238
+ }
239
+ results.length = 0;
240
+ results.push(...finalChecks);
241
+ }
242
+ }
243
+ else if (fixes.length > 0 && !showFix && !jsonOutput) {
244
+ output.writeln();
245
+ output.writeln(output.dim(`Run with --fix to auto-fix ${fixes.length} issue${fixes.length > 1 ? 's' : ''}`));
246
+ }
200
247
  }
201
248
  catch {
202
249
  spinner?.stop();
203
- restoreStdout();
204
250
  if (!jsonOutput)
205
251
  output.writeln(output.error('Failed to run health checks'));
206
252
  }
207
- if (jsonOutput) {
208
- return emitJsonOutput({ results, strict, allowWarnList });
209
- }
210
- if (autoInstall) {
211
- await maybeAutoInstallClaudeCode(results, fixes);
212
- }
213
- renderSummary(results);
214
- if (showFix && fixes.length > 0) {
215
- await runAutoFix(results, fixes, checksToRun);
253
+ finally {
254
+ restoreStdout();
216
255
  }
217
- else if (fixes.length > 0 && !showFix) {
218
- output.writeln();
219
- output.writeln(output.dim(`Run with --fix to auto-fix ${fixes.length} issue${fixes.length > 1 ? 's' : ''}`));
256
+ if (jsonOutput) {
257
+ return emitJsonOutput({
258
+ results,
259
+ strict,
260
+ allowWarnList,
261
+ fixesApplied,
262
+ zombieScan,
263
+ claudeCodeInstall,
264
+ });
220
265
  }
221
266
  return finalize({ results, strict, allowWarnList });
222
267
  },
@@ -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 },
@@ -95,6 +95,27 @@ export function logBridgeError(context, err, opts) {
95
95
  const msg = errorDetail(err);
96
96
  console.error(`[moflo] ${context}: ${msg}`);
97
97
  }
98
+ /**
99
+ * Recognises the node:sqlite "operation on closed handle" error shape.
100
+ *
101
+ * #1123 — A concurrent `withDb` call's `checkBridgeCoherence` can fire
102
+ * `shutdownBridge()` between our `getDb(registry)` and `fn(ctx, registry)`,
103
+ * closing the underlying `DatabaseSync`. Our previously-captured `ctx.db`
104
+ * then throws `ERR_INVALID_STATE: database is not open` on the next op.
105
+ *
106
+ * The operation hadn't started its mutation yet, so a single retry against a
107
+ * fresh registry is safe (matches the `withBusyRetry` shape for SQLITE_BUSY).
108
+ * Bounded to one retry so a *genuinely* broken DB still surfaces — we don't
109
+ * want to mask a registry that can't be re-acquired.
110
+ */
111
+ function isStaleHandleError(err) {
112
+ if (!err || typeof err !== 'object')
113
+ return false;
114
+ const e = err;
115
+ if (e.code === 'ERR_INVALID_STATE')
116
+ return true;
117
+ return typeof e.message === 'string' && /database is not open/i.test(e.message);
118
+ }
98
119
  /**
99
120
  * Treats an error as a SQLITE_BUSY lock-contention failure if either the
100
121
  * error code or message indicates it. Belt-and-suspenders around node:sqlite,
@@ -456,6 +477,9 @@ async function checkBridgeCoherence(dbPath) {
456
477
  * self-fire is suppressed.
457
478
  */
458
479
  export async function withDb(dbPath, fn) {
480
+ return withDbInner(dbPath, fn, 0);
481
+ }
482
+ async function withDbInner(dbPath, fn, attempt) {
459
483
  await checkBridgeCoherence(dbPath);
460
484
  const registry = await getRegistry(dbPath);
461
485
  if (!registry)
@@ -510,6 +534,18 @@ export async function withDb(dbPath, fn) {
510
534
  return result;
511
535
  }
512
536
  catch (err) {
537
+ // #1123 — stale-handle race: a concurrent withDb's coherence check tore
538
+ // the registry down between our getDb() and fn() execution, closing the
539
+ // underlying DatabaseSync. Drop the dead handle and retry once against a
540
+ // freshly-acquired registry. The first attempt threw BEFORE its mutation
541
+ // landed (node:sqlite errors at prepare/exec time, not mid-statement), so
542
+ // a retry is idempotent. Bounded to one retry so a genuinely-unrecoverable
543
+ // bridge (e.g. corrupt file, missing module) still surfaces as a null
544
+ // return + logged error, not an infinite loop.
545
+ if (attempt === 0 && isStaleHandleError(err)) {
546
+ await shutdownBridge();
547
+ return await withDbInner(dbPath, fn, attempt + 1);
548
+ }
513
549
  logBridgeError('bridge operation failed', err);
514
550
  return null;
515
551
  }
@@ -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.1';
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.1",
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.0",
98
+ "moflo": "^4.10.2",
99
99
  "tsx": "^4.21.0",
100
100
  "typescript": "^5.9.3",
101
101
  "vitest": "^4.0.0"