moflo 4.12.3 → 4.12.4-rc.2

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.
Files changed (98) hide show
  1. package/.claude/agents/core/coder.md +2 -3
  2. package/.claude/agents/core/researcher.md +3 -4
  3. package/.claude/agents/core/reviewer.md +6 -8
  4. package/.claude/guidance/shipped/moflo-spell-engine.md +13 -10
  5. package/.claude/helpers/gate-hook.mjs +67 -2
  6. package/.claude/helpers/gate.cjs +173 -12
  7. package/.claude/helpers/hook-handler.cjs +0 -0
  8. package/.claude/helpers/prompt-hook.mjs +0 -0
  9. package/.claude/skills/connector-builder/SKILL.md +5 -4
  10. package/.claude/skills/fl/phases.md +1 -1
  11. package/.claude/skills/fl/sdd.md +1 -1
  12. package/.claude/skills/spell-builder/SKILL.md +5 -4
  13. package/.claude/skills/spell-builder/architecture.md +4 -4
  14. package/.claude/skills/spell-builder/connectors/github-cli/README.md +22 -17
  15. package/.claude/skills/spell-builder/connectors/http/README.md +18 -11
  16. package/.claude/skills/spell-builder/connectors/local-outlook/README.md +8 -7
  17. package/.claude/skills/spell-builder/connectors/playwright/README.md +10 -8
  18. package/.claude/skills/spell-builder/steps/agent/README.md +25 -21
  19. package/.claude/skills/verify/SKILL.md +24 -3
  20. package/bin/gate-hook.mjs +67 -2
  21. package/bin/gate.cjs +173 -12
  22. package/bin/generate-code-map.mjs +70 -17
  23. package/bin/index-all.mjs +38 -0
  24. package/bin/index-guidance.mjs +8 -41
  25. package/bin/index-patterns.mjs +2 -2
  26. package/bin/index-tests.mjs +6 -4
  27. package/bin/lib/guidance-config.mjs +119 -0
  28. package/bin/lib/index-fingerprint.mjs +0 -0
  29. package/dist/src/cli/commands/agent.js +1 -89
  30. package/dist/src/cli/commands/analyze.js +1 -1
  31. package/dist/src/cli/commands/benchmark.js +11 -8
  32. package/dist/src/cli/commands/claims.js +2 -2
  33. package/dist/src/cli/commands/completions.js +1 -1
  34. package/dist/src/cli/commands/config.js +256 -96
  35. package/dist/src/cli/commands/deployment.js +6 -6
  36. package/dist/src/cli/commands/diagnose.js +1 -1
  37. package/dist/src/cli/commands/doctor-checks-config.js +18 -22
  38. package/dist/src/cli/commands/doctor-checks-deep.js +1 -1
  39. package/dist/src/cli/commands/doctor-fixes.js +36 -11
  40. package/dist/src/cli/commands/doctor.js +1 -1
  41. package/dist/src/cli/commands/embeddings.js +64 -23
  42. package/dist/src/cli/commands/github.js +1 -1
  43. package/dist/src/cli/commands/guidance.js +10 -10
  44. package/dist/src/cli/commands/hive-mind.js +8 -70
  45. package/dist/src/cli/commands/hooks.js +32 -219
  46. package/dist/src/cli/commands/mcp.js +5 -2
  47. package/dist/src/cli/commands/memory.js +59 -46
  48. package/dist/src/cli/commands/neural.js +21 -21
  49. package/dist/src/cli/commands/performance.js +14 -7
  50. package/dist/src/cli/commands/plugins.js +2 -2
  51. package/dist/src/cli/commands/providers.js +2 -2
  52. package/dist/src/cli/commands/retire.js +1 -1
  53. package/dist/src/cli/commands/route.js +1 -1
  54. package/dist/src/cli/commands/security.js +4 -4
  55. package/dist/src/cli/commands/status.js +18 -13
  56. package/dist/src/cli/commands/task.js +8 -0
  57. package/dist/src/cli/config/cli-config-store.js +326 -0
  58. package/dist/src/cli/hooks/workers/index.js +15 -9
  59. package/dist/src/cli/index.js +43 -33
  60. package/dist/src/cli/init/claudemd-generator.js +3 -3
  61. package/dist/src/cli/init/helpers-generator.js +203 -20
  62. package/dist/src/cli/init/moflo-yaml-template.js +11 -3
  63. package/dist/src/cli/init/settings-generator.js +10 -2
  64. package/dist/src/cli/mcp-client.js +12 -2
  65. package/dist/src/cli/mcp-server.js +5 -23
  66. package/dist/src/cli/mcp-tools/analysis-tools.js +103 -0
  67. package/dist/src/cli/mcp-tools/coverage-tools.js +206 -0
  68. package/dist/src/cli/mcp-tools/hive-mind-tools.js +71 -0
  69. package/dist/src/cli/mcp-tools/hooks-tools.js +25 -9
  70. package/dist/src/cli/mcp-tools/memory-admin-tools.js +538 -0
  71. package/dist/src/cli/mcp-tools/memory-tools.js +26 -5
  72. package/dist/src/cli/mcp-tools/neural-tools.js +56 -87
  73. package/dist/src/cli/mcp-tools/performance-tools.js +76 -39
  74. package/dist/src/cli/mcp-tools/progress-tools.js +142 -0
  75. package/dist/src/cli/mcp-tools/security-tools.js +1 -1
  76. package/dist/src/cli/mcp-tools/session-tools.js +116 -0
  77. package/dist/src/cli/mcp-tools/synthetic.js +74 -0
  78. package/dist/src/cli/mcp-tools/system-tools.js +126 -78
  79. package/dist/src/cli/mcp-tools/task-tools.js +101 -1
  80. package/dist/src/cli/movector/coverage-router.js +48 -6
  81. package/dist/src/cli/movector/diff-classifier.js +8 -6
  82. package/dist/src/cli/movector/flash-attention.js +1 -1
  83. package/dist/src/cli/movector/vector-db.js +1 -1
  84. package/dist/src/cli/parser.js +81 -12
  85. package/dist/src/cli/services/daemon-lock.js +5 -2
  86. package/dist/src/cli/services/hook-block-hash.js +3 -1
  87. package/dist/src/cli/services/moflo-version.js +62 -0
  88. package/dist/src/cli/services/movector-training.js +1 -1
  89. package/dist/src/cli/services/spell-gate.js +6 -4
  90. package/dist/src/cli/services/worker-daemon.js +37 -7
  91. package/dist/src/cli/shared/utils/load-average.js +59 -0
  92. package/dist/src/cli/spells/commands/agent-command.js +55 -25
  93. package/dist/src/cli/suggest.js +1 -1
  94. package/dist/src/cli/swarm/swarm-persistence.js +112 -0
  95. package/dist/src/cli/swarm/unified-coordinator.js +118 -3
  96. package/dist/src/cli/version.js +1 -1
  97. package/package.json +5 -3
  98. package/dist/src/cli/mcp-tools/github-tools.js +0 -318
@@ -2,17 +2,18 @@
2
2
  * V3 CLI Performance Command
3
3
  * Performance profiling, benchmarking, optimization, metrics
4
4
  *
5
- * Created with ❤️ by motailz.com
5
+ * Created with ❤️ by cielolimitada.com
6
6
  */
7
7
  import { output } from '../output.js';
8
+ import { readLoadAverage, NOT_MEASURED } from '../shared/utils/load-average.js';
8
9
  // Benchmark subcommand - REAL measurements
9
10
  const benchmarkCommand = {
10
11
  name: 'benchmark',
11
12
  description: 'Run performance benchmarks',
12
13
  options: [
13
14
  { name: 'suite', short: 's', type: 'string', description: 'Benchmark suite: all, wasm, neural, memory, search', default: 'all' },
14
- { name: 'iterations', short: 'i', type: 'number', description: 'Number of iterations', default: '100' },
15
- { name: 'warmup', short: 'w', type: 'number', description: 'Warmup iterations', default: '10' },
15
+ { name: 'iterations', short: 'i', type: 'number', description: 'Number of iterations', default: 100 },
16
+ { name: 'warmup', short: 'w', type: 'number', description: 'Warmup iterations', default: 10 },
16
17
  { name: 'output', short: 'o', type: 'string', description: 'Output format: text, json, csv', default: 'text' },
17
18
  ],
18
19
  examples: [
@@ -213,7 +214,7 @@ const profileCommand = {
213
214
  description: 'Profile application performance',
214
215
  options: [
215
216
  { name: 'type', short: 't', type: 'string', description: 'Profile type: cpu, memory, io, all', default: 'all' },
216
- { name: 'duration', short: 'd', type: 'number', description: 'Duration in seconds', default: '30' },
217
+ { name: 'duration', short: 'd', type: 'number', description: 'Duration in seconds', default: 30 },
217
218
  { name: 'output', short: 'o', type: 'string', description: 'Output file for profile data' },
218
219
  ],
219
220
  examples: [
@@ -302,7 +303,10 @@ const metricsCommand = {
302
303
  const memUsage = process.memoryUsage();
303
304
  const cpuUsage = process.cpuUsage();
304
305
  const uptime = process.uptime();
305
- const loadAvg = os.loadavg();
306
+ // null where the platform has none (#1358). Rendered as "not measured"
307
+ // below rather than as 0.00 — a reading that does not exist must not read
308
+ // as an idle machine.
309
+ const loadAvg = readLoadAverage(os.loadavg(), os.platform());
306
310
  const freeMem = os.freemem();
307
311
  const totalMem = os.totalmem();
308
312
  // Calculate real metrics
@@ -454,7 +458,10 @@ const metricsCommand = {
454
458
  ],
455
459
  });
456
460
  output.writeln();
457
- output.writeln(output.dim(`Load Average: ${loadAvg.map(l => l.toFixed(2)).join(', ')}`));
461
+ const loadAvgLine = loadAvg === null
462
+ ? `${NOT_MEASURED} (${os.platform()} has no load average)`
463
+ : loadAvg.map(l => l.toFixed(2)).join(', ');
464
+ output.writeln(output.dim(`Load Average: ${loadAvgLine}`));
458
465
  output.writeln(output.dim(`CPUs: ${os.cpus().length} | Platform: ${os.platform()} ${os.release()}`));
459
466
  return { success: true };
460
467
  },
@@ -571,7 +578,7 @@ export const performanceCommand = {
571
578
  'Memory: Int8 quantized weight storage',
572
579
  ]);
573
580
  output.writeln();
574
- output.writeln(output.dim('Created with ❤️ by motailz.com'));
581
+ output.writeln(output.dim('Created with ❤️ by cielolimitada.com'));
575
582
  return { success: true };
576
583
  },
577
584
  };
@@ -3,7 +3,7 @@
3
3
  * Plugin management, installation, and lifecycle
4
4
  * Now uses IPFS-based decentralized registry for discovery
5
5
  *
6
- * Created with ❤️ by motailz.com
6
+ * Created with ❤️ by cielolimitada.com
7
7
  */
8
8
  import { output } from '../output.js';
9
9
  import { createPluginDiscoveryService, searchPlugins, getPluginSearchSuggestions, getFeaturedPlugins, getOfficialPlugins, } from '../plugins/store/index.js';
@@ -807,7 +807,7 @@ export const pluginsCommand = {
807
807
  ]);
808
808
  output.writeln();
809
809
  output.writeln(output.dim('Run "flo plugins list --official" to see all official plugins'));
810
- output.writeln(output.dim('Created with ❤️ by motailz.com'));
810
+ output.writeln(output.dim('Created with ❤️ by cielolimitada.com'));
811
811
  return { success: true };
812
812
  },
813
813
  };
@@ -2,7 +2,7 @@
2
2
  * V3 CLI Providers Command
3
3
  * Manage AI providers, models, and configurations
4
4
  *
5
- * Created with ❤️ by motailz.com
5
+ * Created with ❤️ by cielolimitada.com
6
6
  */
7
7
  import { output } from '../output.js';
8
8
  // List subcommand
@@ -224,7 +224,7 @@ export const providersCommand = {
224
224
  'Agentic Flow (optimized ONNX with SIMD)',
225
225
  ]);
226
226
  output.writeln();
227
- output.writeln(output.dim('Created with ❤️ by motailz.com'));
227
+ output.writeln(output.dim('Created with ❤️ by cielolimitada.com'));
228
228
  return { success: true };
229
229
  },
230
230
  };
@@ -13,7 +13,7 @@
13
13
  * `retired-files.json` live at the moflo package root and don't ship to
14
14
  * consumer projects.
15
15
  *
16
- * Created with motailz.com
16
+ * Created with cielolimitada.com
17
17
  */
18
18
  import { output } from '../output.js';
19
19
  import { spawnSync } from 'child_process';
@@ -8,7 +8,7 @@
8
8
  * - Confidence scoring
9
9
  * - Learning from feedback
10
10
  *
11
- * Created with love by motailz.com
11
+ * Created with love by cielolimitada.com
12
12
  */
13
13
  import { output } from '../output.js';
14
14
  import { errorDetail } from '../shared/utils/error-detail.js';
@@ -2,7 +2,7 @@
2
2
  * V3 CLI Security Command
3
3
  * Security scanning, CVE detection, threat modeling, vulnerability management
4
4
  *
5
- * Created with ❤️ by motailz.com
5
+ * Created with ❤️ by cielolimitada.com
6
6
  */
7
7
  import { output } from '../output.js';
8
8
  // Scan subcommand
@@ -331,7 +331,7 @@ const auditCommand = {
331
331
  description: 'Security audit logging and compliance',
332
332
  options: [
333
333
  { name: 'action', short: 'a', type: 'string', description: 'Action: log, list, export, clear', default: 'list' },
334
- { name: 'limit', short: 'l', type: 'number', description: 'Number of entries to show', default: '20' },
334
+ { name: 'limit', short: 'l', type: 'number', description: 'Number of entries to show', default: 20 },
335
335
  { name: 'filter', short: 'f', type: 'string', description: 'Filter by event type' },
336
336
  ],
337
337
  examples: [
@@ -412,7 +412,7 @@ const defendCommand = {
412
412
  { name: 'input', short: 'i', type: 'string', description: 'Input text to scan for threats' },
413
413
  { name: 'file', short: 'f', type: 'string', description: 'File to scan for threats' },
414
414
  { name: 'quick', short: 'Q', type: 'boolean', description: 'Quick scan (faster, less detailed)' },
415
- { name: 'learn', short: 'l', type: 'boolean', description: 'Enable learning mode', default: 'true' },
415
+ { name: 'learn', short: 'l', type: 'boolean', description: 'Enable learning mode', default: true },
416
416
  { name: 'stats', short: 's', type: 'boolean', description: 'Show detection statistics' },
417
417
  { name: 'output', short: 'o', type: 'string', description: 'Output format: text, json', default: 'text' },
418
418
  ],
@@ -566,7 +566,7 @@ export const securityCommand = {
566
566
  output.writeln();
567
567
  output.writeln('Use --help with subcommands for more info');
568
568
  output.writeln();
569
- output.writeln(output.dim('Created with ❤️ by motailz.com'));
569
+ output.writeln(output.dim('Created with ❤️ by cielolimitada.com'));
570
570
  return { success: true };
571
571
  },
572
572
  };
@@ -79,8 +79,11 @@ async function getSystemStatus() {
79
79
  catch {
80
80
  // MCP not running
81
81
  }
82
- // Get memory status
83
- const memoryStatus = await callMCPTool('memory_stats', {});
82
+ // #1349 — this used to call `memory_stats`, which returns none of
83
+ // `entries` / `size` / `performance`; dereferencing `.performance` threw
84
+ // and dropped the whole command into its all-zeros fallback. Use the
85
+ // detailed-stats tool, whose shape this actually needs.
86
+ const memoryStatus = await callMCPTool('memory_detailed-stats', {});
84
87
  // Get task status
85
88
  const taskStatus = await callMCPTool('task_summary', {});
86
89
  return {
@@ -103,8 +106,7 @@ async function getSystemStatus() {
103
106
  size: formatBytes(memoryStatus.size),
104
107
  backend: memoryStatus.backend,
105
108
  performance: {
106
- searchTime: memoryStatus.performance.avgSearchTime,
107
- cacheHitRate: memoryStatus.performance.cacheHitRate
109
+ searchTime: memoryStatus.performance.avgSearchTime
108
110
  }
109
111
  },
110
112
  tasks: taskStatus,
@@ -131,7 +133,7 @@ async function getSystemStatus() {
131
133
  entries: 0,
132
134
  size: '0 B',
133
135
  backend: 'none',
134
- performance: { searchTime: 0, cacheHitRate: 0 }
136
+ performance: { searchTime: null }
135
137
  },
136
138
  tasks: { total: 0, pending: 0, running: 0, completed: 0, failed: 0 },
137
139
  performance: {
@@ -211,8 +213,8 @@ function displayStatus(status) {
211
213
  { property: 'Backend', value: status.memory.backend },
212
214
  { property: 'Entries', value: status.memory.entries },
213
215
  { property: 'Size', value: status.memory.size },
214
- { property: 'Search Time', value: `${status.memory.performance.searchTime.toFixed(2)}ms` },
215
- { property: 'Cache Hit Rate', value: `${(status.memory.performance.cacheHitRate * 100).toFixed(1)}%` }
216
+ // null means the probe never ran — say so rather than print 0.00ms.
217
+ { property: 'Search Time', value: status.memory.performance.searchTime === null ? 'not measured' : `${status.memory.performance.searchTime.toFixed(2)}ms` }
216
218
  ]
217
219
  });
218
220
  output.writeln();
@@ -456,7 +458,9 @@ const tasksCommand = {
456
458
  { key: 'progress', header: 'Progress', width: 10 }
457
459
  ],
458
460
  data: result.tasks.map(t => ({
459
- id: t.id,
461
+ // task_list projects the coordinator id as `taskId`; reading `id`
462
+ // left the ID column blank on every row (#1349).
463
+ id: t.taskId,
460
464
  type: t.type,
461
465
  status: formatHealth(t.status),
462
466
  priority: t.priority,
@@ -483,7 +487,7 @@ const memoryCommand = {
483
487
  description: 'Show detailed memory status',
484
488
  action: async (ctx) => {
485
489
  try {
486
- const result = await callMCPTool('memory_detailed-stats', {});
490
+ const result = await callMCPTool('memory_detailed-stats', { measureLatency: true });
487
491
  if (ctx.flags.format === 'json') {
488
492
  output.printJson(result);
489
493
  return { success: true, data: result };
@@ -500,7 +504,7 @@ const memoryCommand = {
500
504
  { property: 'Backend', value: result.backend },
501
505
  { property: 'Total Entries', value: result.entries.toLocaleString() },
502
506
  { property: 'Storage Size', value: formatBytes(result.size) },
503
- { property: 'HNSW Index', value: result.performance.hnswEnabled ? 'Enabled' : 'Disabled' }
507
+ { property: 'HNSW Index', value: result.performance.hnswEnabled ? 'Active' : 'Not built (search falls back to scan)' }
504
508
  ]
505
509
  });
506
510
  output.writeln();
@@ -511,9 +515,10 @@ const memoryCommand = {
511
515
  { key: 'value', header: 'Value', width: 20, align: 'right' }
512
516
  ],
513
517
  data: [
514
- { metric: 'Avg Search Time', value: `${result.performance.avgSearchTime.toFixed(2)}ms` },
515
- { metric: 'Avg Write Time', value: `${result.performance.avgWriteTime.toFixed(2)}ms` },
516
- { metric: 'Cache Hit Rate', value: `${(result.performance.cacheHitRate * 100).toFixed(1)}%` }
518
+ // Only metrics the store actually measures — write latency and
519
+ // cache hit rate are not tracked anywhere, so they are not shown.
520
+ { metric: 'Avg Search Time', value: result.performance.avgSearchTime === null ? 'unavailable' : `${result.performance.avgSearchTime.toFixed(2)}ms` },
521
+ { metric: 'Indexed Vectors', value: result.performance.indexedVectors.toLocaleString() }
517
522
  ]
518
523
  });
519
524
  return { success: true, data: result };
@@ -594,6 +594,14 @@ const retryCommand = {
594
594
  taskId,
595
595
  resetState
596
596
  });
597
+ // The handler reports an unknown/unretryable task by returning
598
+ // success:false, not by throwing — so the catch below never sees it.
599
+ // Without this check, `task retry does-not-exist` printed
600
+ // "Task retried / New task ID: undefined" and exited 0 (#1349).
601
+ if (!result.success) {
602
+ output.printError(`Failed to retry task: ${result.error ?? 'unknown error'}`);
603
+ return { success: false, exitCode: 1, data: result };
604
+ }
597
605
  output.writeln();
598
606
  output.printSuccess(`Task ${taskId} retried`);
599
607
  output.printInfo(`New task ID: ${result.newTaskId}`);
@@ -0,0 +1,326 @@
1
+ /**
2
+ * CLI JSON config store — the file behind `flo config init|get|set|reset|
3
+ * export|import|providers`, and the file the healer's `Config File` check
4
+ * looks for.
5
+ *
6
+ * Two neighbouring config surfaces exist; this module is deliberately none of
7
+ * them:
8
+ * - `src/cli/config/moflo-config.ts` owns **moflo.yaml** (gates, sdd,
9
+ * daemon, memory backend). `flo config show|generate` operate on that.
10
+ * - `src/cli/shared/core/config/loader.ts` owns the `SystemConfig` schema
11
+ * loaded from bare `moflo.config.json`-style files in cwd / `~/.moflo`.
12
+ *
13
+ * This module owns the project-local `.moflo/config.json` — the first entry in
14
+ * the doctor's `Config File` candidate list and the path its auto-fix creates.
15
+ *
16
+ * Before this module existed the whole `flo config` family was display-only:
17
+ * `init` printed "Creating claude-flow.config.json..." and returned
18
+ * `success: true` without a single filesystem call, so `flo doctor --fix`
19
+ * reported the `Config File` warning as fixed on every run while the warning
20
+ * never cleared. Same defect class as the synthetic MCP responses labelled at
21
+ * the call site in #1324/#1325: a success report for work that never happened.
22
+ *
23
+ * @module moflo/cli/config/cli-config-store
24
+ */
25
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
26
+ import { dirname, join } from 'node:path';
27
+ import { atomicWriteFileSync } from '../shared/utils/atomic-file-write.js';
28
+ import { mofloDir } from '../services/moflo-paths.js';
29
+ /**
30
+ * Candidate filenames, canonical first. `doctor-checks-config.ts:checkConfigFile`
31
+ * imports this list rather than keeping its own, so the healer's check and this
32
+ * store cannot disagree about whether a config exists — the mismatch that let
33
+ * the old auto-fix "succeed" against a file the check couldn't see.
34
+ *
35
+ * `claude-flow.*` entries are LEGACY-CONFIG: pre-#699 names, still read so
36
+ * consumers upgrading from older moflo builds keep their existing file.
37
+ */
38
+ export const CLI_CONFIG_CANDIDATES = [
39
+ join('.moflo', 'config.json'),
40
+ 'moflo.config.json',
41
+ 'claude-flow.config.json', // LEGACY-CONFIG: pre-#699 fallback
42
+ '.claude-flow.json', // LEGACY-CONFIG: pre-#699 fallback
43
+ ];
44
+ /**
45
+ * YAML config filenames. Existence-checked only — no yaml parse — and shared
46
+ * with the doctor for the same anti-drift reason as the JSON list above.
47
+ */
48
+ export const CLI_CONFIG_YAML_CANDIDATES = [
49
+ join('.moflo', 'config.yaml'),
50
+ join('.moflo', 'config.yml'),
51
+ 'moflo.config.yaml',
52
+ 'claude-flow.config.yaml', // LEGACY-CONFIG: pre-#699 fallback
53
+ ];
54
+ /** Absolute path of the canonical config file for a project root. */
55
+ export function cliConfigPath(projectRoot) {
56
+ return join(mofloDir(projectRoot), 'config.json');
57
+ }
58
+ /**
59
+ * Absolute path of the config file that actually exists, canonical first, or
60
+ * `null` when the project has none.
61
+ */
62
+ export function findCliConfigFile(projectRoot) {
63
+ for (const candidate of CLI_CONFIG_CANDIDATES) {
64
+ const path = join(projectRoot, candidate);
65
+ if (existsSync(path))
66
+ return path;
67
+ }
68
+ return null;
69
+ }
70
+ /** Default configuration. Values match what `flo config` reported pre-fix. */
71
+ export function defaultCliConfig(opts = {}) {
72
+ return {
73
+ version: '3.0.0',
74
+ v3Mode: opts.v3 ?? true,
75
+ sparc: opts.sparc ?? false,
76
+ agents: {
77
+ defaultType: 'coder',
78
+ maxConcurrent: 15,
79
+ autoSpawn: true,
80
+ timeout: 300,
81
+ },
82
+ swarm: {
83
+ topology: 'hybrid',
84
+ maxAgents: 15,
85
+ autoScale: true,
86
+ coordinationStrategy: 'consensus',
87
+ },
88
+ memory: {
89
+ backend: 'hybrid',
90
+ path: './data/memory',
91
+ cacheSize: 256,
92
+ enableHNSW: true,
93
+ },
94
+ mcp: {
95
+ transport: 'stdio',
96
+ autoStart: true,
97
+ tools: 'all',
98
+ },
99
+ providers: [
100
+ { name: 'anthropic', model: 'claude-sonnet-5', priority: 1, enabled: true },
101
+ { name: 'openrouter', model: 'claude-sonnet-5', priority: 2, enabled: false },
102
+ { name: 'ollama', model: 'llama3.2', priority: 3, enabled: false },
103
+ { name: 'gemini', model: 'gemini-2.0-flash', priority: 4, enabled: false },
104
+ ],
105
+ };
106
+ }
107
+ /** Thrown when a config file exists but cannot be parsed. */
108
+ export class CliConfigParseError extends Error {
109
+ path;
110
+ constructor(path, cause) {
111
+ super(`Invalid JSON in ${path}: ${cause instanceof Error ? cause.message : String(cause)}`);
112
+ this.path = path;
113
+ this.name = 'CliConfigParseError';
114
+ }
115
+ }
116
+ function isPlainObject(value) {
117
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
118
+ }
119
+ /** Deep-merge `patch` over `base`. Arrays are replaced wholesale, not merged. */
120
+ function deepMerge(base, patch) {
121
+ if (!isPlainObject(patch))
122
+ return base;
123
+ if (!isPlainObject(base))
124
+ return patch;
125
+ const result = { ...base };
126
+ for (const [key, value] of Object.entries(patch)) {
127
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype')
128
+ continue;
129
+ const current = result[key];
130
+ result[key] = isPlainObject(value) && isPlainObject(current) ? deepMerge(current, value) : value;
131
+ }
132
+ return result;
133
+ }
134
+ /**
135
+ * Load the project's config, merged over defaults. `path` is `null` when no
136
+ * file exists — callers get usable defaults either way, and can tell the
137
+ * difference.
138
+ *
139
+ * @throws {CliConfigParseError} when a config file exists but is not valid JSON.
140
+ */
141
+ export function loadCliConfig(projectRoot) {
142
+ const path = findCliConfigFile(projectRoot);
143
+ if (!path)
144
+ return { config: defaultCliConfig(), path: null };
145
+ let parsed;
146
+ try {
147
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
148
+ }
149
+ catch (err) {
150
+ throw new CliConfigParseError(path, err);
151
+ }
152
+ return { config: coerceShape(deepMerge(defaultCliConfig(), parsed)), path };
153
+ }
154
+ /**
155
+ * Restore any section whose type the file contradicts. `deepMerge` is
156
+ * structural, not validating, so a hand-edited `"providers": "x"` would
157
+ * survive as a `CliConfig` that lies about its own type and throws the first
158
+ * time something calls `.find` on it. Falling back to the default for just the
159
+ * malformed section keeps the rest of the user's file intact.
160
+ */
161
+ function coerceShape(config) {
162
+ const defaults = defaultCliConfig();
163
+ const repaired = { ...config };
164
+ if (!Array.isArray(repaired.providers))
165
+ repaired.providers = defaults.providers;
166
+ for (const section of ['agents', 'swarm', 'memory', 'mcp']) {
167
+ if (!isPlainObject(repaired[section])) {
168
+ repaired[section] = defaults[section];
169
+ }
170
+ }
171
+ return repaired;
172
+ }
173
+ /**
174
+ * Write `config` and return the path written. Writes to the existing file when
175
+ * there is one (so a consumer's root-level `moflo.config.json` keeps being the
176
+ * file they edit), otherwise creates the canonical `.moflo/config.json`.
177
+ */
178
+ export function saveCliConfig(projectRoot, config, opts = {}) {
179
+ const target = opts.path ?? findCliConfigFile(projectRoot) ?? cliConfigPath(projectRoot);
180
+ writeJsonFile(target, config);
181
+ return target;
182
+ }
183
+ /** Write `content` to `path` atomically, creating parent dirs as needed. */
184
+ export function writeTextFile(path, content) {
185
+ // recursive mkdir is already idempotent — no existence check needed.
186
+ mkdirSync(dirname(path), { recursive: true });
187
+ atomicWriteFileSync(path, content);
188
+ }
189
+ /** Serialize `value` as pretty JSON to `path`, creating parent dirs. */
190
+ export function writeJsonFile(path, value) {
191
+ writeTextFile(path, `${JSON.stringify(value, null, 2)}\n`);
192
+ }
193
+ /**
194
+ * Split a dot-notation key, rejecting prototype-polluting segments and empty
195
+ * ones. Empty segments are rejected rather than filtered out: silently
196
+ * accepting `swarm..topology` contradicts the loud-failure contract that
197
+ * `setConfigValue` relies on to catch typos.
198
+ */
199
+ function splitKey(key) {
200
+ const segments = key.split('.');
201
+ if (segments.length === 0 || segments.some((s) => s.length === 0))
202
+ return null;
203
+ if (segments.some((s) => s === '__proto__' || s === 'constructor' || s === 'prototype'))
204
+ return null;
205
+ return segments;
206
+ }
207
+ /**
208
+ * Read a dot-notation key (`swarm.topology`, `providers.0.enabled`).
209
+ * `found: false` for keys the config does not define.
210
+ */
211
+ export function getConfigValue(config, key) {
212
+ const segments = splitKey(key);
213
+ if (!segments)
214
+ return { found: false };
215
+ let current = config;
216
+ for (const segment of segments) {
217
+ if (current === null || typeof current !== 'object')
218
+ return { found: false };
219
+ if (Array.isArray(current)) {
220
+ const index = Number(segment);
221
+ if (!Number.isInteger(index) || index < 0 || index >= current.length)
222
+ return { found: false };
223
+ current = current[index];
224
+ }
225
+ else {
226
+ if (!Object.prototype.hasOwnProperty.call(current, segment))
227
+ return { found: false };
228
+ current = current[segment];
229
+ }
230
+ }
231
+ return { found: true, value: current };
232
+ }
233
+ /**
234
+ * Coerce a CLI string to the type of the value already at that key, so
235
+ * `flo config set swarm.maxAgents 20` stores the number 20, not "20".
236
+ */
237
+ function coerceValue(raw, current) {
238
+ if (typeof current === 'number') {
239
+ const parsed = Number(raw);
240
+ if (!Number.isFinite(parsed))
241
+ return { ok: false, error: `expected a number, got "${raw}"` };
242
+ return { ok: true, value: parsed };
243
+ }
244
+ if (typeof current === 'boolean') {
245
+ if (/^(true|1|yes|on)$/i.test(raw))
246
+ return { ok: true, value: true };
247
+ if (/^(false|0|no|off)$/i.test(raw))
248
+ return { ok: true, value: false };
249
+ return { ok: false, error: `expected a boolean, got "${raw}"` };
250
+ }
251
+ if (typeof current === 'object' && current !== null) {
252
+ return { ok: false, error: 'is a section, not a value — set one of its keys instead' };
253
+ }
254
+ return { ok: true, value: raw };
255
+ }
256
+ /**
257
+ * Set a dot-notation key in place. Rejects keys the config does not already
258
+ * define — a typo should fail loudly, not silently create a key nothing reads.
259
+ */
260
+ export function setConfigValue(config, key, raw) {
261
+ const segments = splitKey(key);
262
+ if (!segments)
263
+ return { ok: false, error: `invalid key: "${key}"` };
264
+ const existing = getConfigValue(config, key);
265
+ if (!existing.found)
266
+ return { ok: false, error: `unknown key: ${key}` };
267
+ const coerced = coerceValue(raw, existing.value);
268
+ if (!coerced.ok)
269
+ return coerced;
270
+ const parentSegments = segments.slice(0, -1);
271
+ const leaf = segments[segments.length - 1];
272
+ const parent = parentSegments.length === 0
273
+ ? { found: true, value: config }
274
+ : getConfigValue(config, parentSegments.join('.'));
275
+ if (!parent.found || parent.value === null || typeof parent.value !== 'object') {
276
+ return { ok: false, error: `unknown key: ${key}` };
277
+ }
278
+ if (Array.isArray(parent.value)) {
279
+ parent.value[Number(leaf)] = coerced.value;
280
+ }
281
+ else {
282
+ parent.value[leaf] = coerced.value;
283
+ }
284
+ return { ok: true, value: coerced.value };
285
+ }
286
+ /**
287
+ * Flatten to dotted leaf keys for tabular display. An EMPTY object or array is
288
+ * itself a leaf — recursing over zero entries would otherwise drop the key
289
+ * entirely, so `providers: []` would vanish from `flo config get` and read as
290
+ * "not configured" rather than "configured empty".
291
+ */
292
+ export function flattenConfig(value, prefix = '') {
293
+ const flat = {};
294
+ const key = prefix.replace(/\.$/, '');
295
+ if (Array.isArray(value)) {
296
+ if (value.length === 0) {
297
+ flat[key] = '[]';
298
+ return flat;
299
+ }
300
+ value.forEach((item, index) => Object.assign(flat, flattenConfig(item, `${prefix}${index}.`)));
301
+ return flat;
302
+ }
303
+ if (isPlainObject(value)) {
304
+ const entries = Object.entries(value);
305
+ if (entries.length === 0) {
306
+ flat[key] = '{}';
307
+ return flat;
308
+ }
309
+ for (const [childKey, child] of entries) {
310
+ Object.assign(flat, flattenConfig(child, `${prefix}${childKey}.`));
311
+ }
312
+ return flat;
313
+ }
314
+ flat[key] = value;
315
+ return flat;
316
+ }
317
+ /** Config sections `flo config reset --section` accepts. */
318
+ export const RESETTABLE_SECTIONS = ['agents', 'swarm', 'memory', 'mcp', 'providers', 'all'];
319
+ /** Return `config` with `section` (or everything) restored to defaults. */
320
+ export function resetSection(config, section) {
321
+ const defaults = defaultCliConfig({ v3: config.v3Mode, sparc: config.sparc });
322
+ if (section === 'all')
323
+ return defaults;
324
+ return { ...config, [section]: defaults[section] };
325
+ }
326
+ //# sourceMappingURL=cli-config-store.js.map
@@ -9,6 +9,7 @@ import * as os from 'os';
9
9
  import * as path from 'path';
10
10
  import * as fs from 'fs/promises';
11
11
  import { errorDetail } from '../../shared/utils/error-detail.js';
12
+ import { readLoadAverage } from '../../shared/utils/load-average.js';
12
13
  // ============================================================================
13
14
  // Security Constants
14
15
  // ============================================================================
@@ -502,7 +503,9 @@ export class WorkerManager extends EventEmitter {
502
503
  issues: securityResult?.totalIssues ?? 0,
503
504
  },
504
505
  performance: {
505
- speedup: perfResult?.speedup ?? '1.0x',
506
+ // Null both when the worker has not run yet and when the platform has
507
+ // no load average — in either case there is nothing measured to show.
508
+ loadAvg: perfResult?.cpu?.loadAvg ?? null,
506
509
  },
507
510
  alerts: this.alerts.filter(a => a.severity === AlertSeverity.Critical).slice(-5),
508
511
  lastUpdate: new Date().toISOString(),
@@ -537,8 +540,10 @@ export class WorkerManager extends EventEmitter {
537
540
  const secIcon = data.security.status === 'critical' ? '🚨' :
538
541
  data.security.status === 'warning' ? '⚠️' : '🛡️';
539
542
  parts.push(`${secIcon}${data.security.issues}`);
540
- // Performance
541
- parts.push(`⚡${data.performance.speedup}`);
543
+ // Performance — 'n/a' rather than a zero, for the same reason the field
544
+ // is nullable: a statusline is exactly where an unmeasured value gets
545
+ // read as a measured one.
546
+ parts.push(`⚡${data.performance.loadAvg ?? 'n/a'}`);
542
547
  return parts.join(' │ ');
543
548
  }
544
549
  // =========================================================================
@@ -770,9 +775,9 @@ export function createPerformanceWorker(projectRoot) {
770
775
  const totalMem = os.totalmem();
771
776
  const freeMem = os.freemem();
772
777
  const memPct = Math.round((1 - freeMem / totalMem) * 100);
773
- // CPU load
778
+ // CPU load — null where the platform has none (#1358), never "0.00"
774
779
  const cpus = os.cpus();
775
- const loadAvg = os.loadavg()[0];
780
+ const loadAvg = readLoadAverage(os.loadavg(), os.platform());
776
781
  return {
777
782
  worker: 'performance',
778
783
  success: true,
@@ -786,9 +791,8 @@ export function createPerformanceWorker(projectRoot) {
786
791
  },
787
792
  cpu: {
788
793
  cores: cpus.length,
789
- loadAvg: loadAvg.toFixed(2),
794
+ loadAvg: loadAvg === null ? null : loadAvg[0].toFixed(2),
790
795
  },
791
- speedup: '1.0x', // Placeholder
792
796
  },
793
797
  };
794
798
  };
@@ -800,7 +804,9 @@ export function createHealthWorker(projectRoot) {
800
804
  const freeMem = os.freemem();
801
805
  const memPct = Math.round((1 - freeMem / totalMem) * 100);
802
806
  const uptime = os.uptime();
803
- const loadAvg = os.loadavg();
807
+ // null where the platform has none (#1358) — three zeros would read as an
808
+ // idle machine rather than as an absent reading.
809
+ const loadAvg = readLoadAverage(os.loadavg(), os.platform());
804
810
  // Disk space (cross-platform approximation)
805
811
  let diskPct = 0;
806
812
  let diskFree = 'N/A';
@@ -825,7 +831,7 @@ export function createHealthWorker(projectRoot) {
825
831
  disk: { usedPct: diskPct, free: diskFree },
826
832
  system: {
827
833
  uptime: Math.round(uptime / 3600),
828
- loadAvg: loadAvg.map(l => l.toFixed(2)),
834
+ loadAvg: loadAvg === null ? null : loadAvg.map(l => l.toFixed(2)),
829
835
  platform: os.platform(),
830
836
  arch: os.arch(),
831
837
  },