moflo 4.12.4 → 4.12.5

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 (108) hide show
  1. package/.claude/guidance/shipped/moflo-agent-rules.md +1 -0
  2. package/.claude/guidance/shipped/moflo-claude-swarm-cohesion.md +1 -1
  3. package/.claude/guidance/shipped/moflo-cli-reference.md +4 -4
  4. package/.claude/guidance/shipped/moflo-core-guidance.md +4 -4
  5. package/.claude/guidance/shipped/moflo-guidance-rules.md +21 -0
  6. package/.claude/guidance/shipped/moflo-inline-fixes.md +98 -0
  7. package/.claude/guidance/shipped/moflo-root-cause-discipline.md +10 -10
  8. package/.claude/guidance/shipped/moflo-sdd.md +5 -5
  9. package/.claude/guidance/shipped/moflo-spell-engine.md +1 -1
  10. package/.claude/guidance/shipped/moflo-yaml-reference.md +8 -8
  11. package/.claude/helpers/gate.cjs +46 -7
  12. package/.claude/helpers/pr-create-command.cjs +440 -0
  13. package/.claude/skills/eldar/SKILL.md +1 -1
  14. package/.claude/skills/fl/SKILL.md +6 -6
  15. package/.claude/skills/fl/execution-modes.md +3 -3
  16. package/.claude/skills/fl/phases.md +8 -8
  17. package/.claude/skills/fl/sdd.md +6 -6
  18. package/.claude/skills/guidance/SKILL.md +3 -0
  19. package/.claude/skills/verify/SKILL.md +4 -4
  20. package/bin/gate.cjs +46 -7
  21. package/bin/hooks.mjs +4 -1
  22. package/bin/lib/retired-files.mjs +38 -6
  23. package/bin/lib/shipped-scripts.json +2 -1
  24. package/bin/lib/skill-categories.mjs +81 -17
  25. package/bin/pr-create-command.cjs +440 -0
  26. package/bin/session-start-launcher.mjs +11 -6
  27. package/dist/src/cli/aidefence/domain/entities/threat.js +2 -1
  28. package/dist/src/cli/commands/doctor-checks-deep.js +3 -2
  29. package/dist/src/cli/commands/doctor-checks-memory-access.js +3 -2
  30. package/dist/src/cli/commands/doctor-checks-swarm.js +3 -2
  31. package/dist/src/cli/commands/hooks.js +2 -1
  32. package/dist/src/cli/commands/mcp.js +3 -2
  33. package/dist/src/cli/commands/memory.js +2 -1
  34. package/dist/src/cli/commands/performance.js +2 -1
  35. package/dist/src/cli/commands/retire.js +1 -1
  36. package/dist/src/cli/commands/spell-schedule.js +2 -1
  37. package/dist/src/cli/commands/swarm.js +216 -104
  38. package/dist/src/cli/guidance/headless.js +5 -8
  39. package/dist/src/cli/guidance/retriever.js +27 -6
  40. package/dist/src/cli/guidance/ruvbot-integration.js +2 -1
  41. package/dist/src/cli/guidance/uncertainty.js +2 -1
  42. package/dist/src/cli/hooks/bridge/official-hooks-bridge.js +2 -1
  43. package/dist/src/cli/hooks/mcp/index.js +3 -2
  44. package/dist/src/cli/hooks/reasoningbank/index.js +2 -1
  45. package/dist/src/cli/hooks/registry/index.js +2 -1
  46. package/dist/src/cli/hooks/swarm/index.js +6 -5
  47. package/dist/src/cli/init/claudemd-generator.js +2 -1
  48. package/dist/src/cli/init/helpers-generator.js +1 -1
  49. package/dist/src/cli/init/moflo-yaml-template.js +4 -4
  50. package/dist/src/cli/mcp-client.js +5 -2
  51. package/dist/src/cli/mcp-tools/agent-tools.js +4 -1
  52. package/dist/src/cli/mcp-tools/hive-mind-tools.js +4 -3
  53. package/dist/src/cli/mcp-tools/hooks-tools.js +6 -5
  54. package/dist/src/cli/mcp-tools/json-store.js +3 -2
  55. package/dist/src/cli/mcp-tools/neural-tools.js +2 -1
  56. package/dist/src/cli/mcp-tools/performance-tools.js +2 -1
  57. package/dist/src/cli/mcp-tools/session-tools.js +4 -2
  58. package/dist/src/cli/mcp-tools/spell-tools.js +2 -1
  59. package/dist/src/cli/memory/bridge-core.js +8 -2
  60. package/dist/src/cli/memory/controllers/_shared.js +8 -2
  61. package/dist/src/cli/memory/controllers/nightly-learner.js +2 -1
  62. package/dist/src/cli/memory/domain/services/memory-domain-service.js +2 -1
  63. package/dist/src/cli/memory/entries-write.js +2 -1
  64. package/dist/src/cli/memory/intelligence.js +2 -1
  65. package/dist/src/cli/memory/persistent-sona.js +1 -3
  66. package/dist/src/cli/memory/types.js +2 -3
  67. package/dist/src/cli/memory/verify.js +3 -2
  68. package/dist/src/cli/movector/graph-analyzer.js +14 -3
  69. package/dist/src/cli/neural/pattern-learner.js +4 -2
  70. package/dist/src/cli/neural/reasoning-bank.js +2 -1
  71. package/dist/src/cli/neural/reasoningbank-adapter.js +2 -1
  72. package/dist/src/cli/neural/sona-manager.js +4 -3
  73. package/dist/src/cli/production/monitoring.js +2 -1
  74. package/dist/src/cli/services/daemon-service.js +57 -46
  75. package/dist/src/cli/services/daemon-spell-executor.js +3 -2
  76. package/dist/src/cli/services/headless-worker-executor.js +4 -2
  77. package/dist/src/cli/services/learning-service.js +2 -1
  78. package/dist/src/cli/services/worker-daemon.js +2 -1
  79. package/dist/src/cli/shared/hooks/example-usage.js +3 -2
  80. package/dist/src/cli/shared/hooks/safety/git-commit.js +11 -2
  81. package/dist/src/cli/shared/hooks/session-hooks.js +3 -2
  82. package/dist/src/cli/shared/hooks/task-hooks.js +2 -1
  83. package/dist/src/cli/shared/mcp/session-manager.js +2 -1
  84. package/dist/src/cli/shared/plugins/official/hive-mind-plugin.js +2 -1
  85. package/dist/src/cli/shared/plugins/official/maestro-plugin.js +3 -2
  86. package/dist/src/cli/shared/security/index.js +6 -2
  87. package/dist/src/cli/shared/utils/atomic-file-write.js +9 -1
  88. package/dist/src/cli/shared/utils/id.js +54 -0
  89. package/dist/src/cli/shared/utils/platform.js +36 -5
  90. package/dist/src/cli/spells/commands/destructive-pattern-checker.js +6 -1
  91. package/dist/src/cli/spells/core/dry-run-validator.js +2 -1
  92. package/dist/src/cli/spells/core/interpolation.js +8 -3
  93. package/dist/src/cli/spells/core/permission-resolver.js +2 -2
  94. package/dist/src/cli/spells/core/runner.js +2 -1
  95. package/dist/src/cli/spells/core/shell.js +7 -8
  96. package/dist/src/cli/spells/factory/runner-bridge.js +3 -2
  97. package/dist/src/cli/spells/factory/runner-factory.js +3 -2
  98. package/dist/src/cli/spells/scheduler/scheduler.js +2 -1
  99. package/dist/src/cli/swarm/consensus/gossip.js +2 -1
  100. package/dist/src/cli/swarm/federation-hub.js +5 -4
  101. package/dist/src/cli/swarm/queen-coordinator.js +5 -4
  102. package/dist/src/cli/swarm/shared/events.js +2 -1
  103. package/dist/src/cli/swarm/unified-coordinator.js +3 -2
  104. package/dist/src/cli/version.js +1 -1
  105. package/package.json +2 -2
  106. package/retired-files.json +0 -72
  107. package/dist/src/cli/mcp-tools/coordination-tools.js +0 -86
  108. package/dist/src/cli/shared/security/secure-random.js +0 -142
@@ -10,6 +10,75 @@ import * as path from 'path';
10
10
  import { LEGACY_SWARM_DIR, memoryDbCandidatePaths, mofloDir } from '../services/moflo-paths.js';
11
11
  import { findProjectRoot } from '../services/project-root.js';
12
12
  import { resolveStateRoot } from '../services/project-root.js';
13
+ /**
14
+ * Canonical path first, then the pre-#1168 `.swarm/state.json` fallback, so a
15
+ * consumer who initialised on an older moflo still resolves their swarm.
16
+ * Writes only ever go to `[0]`.
17
+ */
18
+ function swarmStateFileCandidates() {
19
+ const root = findProjectRoot();
20
+ return [
21
+ path.join(mofloDir(root), 'swarm', 'state.json'),
22
+ path.join(root, LEGACY_SWARM_DIR, 'state.json'),
23
+ ];
24
+ }
25
+ function readSwarmState() {
26
+ for (const file of swarmStateFileCandidates()) {
27
+ if (!fs.existsSync(file))
28
+ continue;
29
+ try {
30
+ return JSON.parse(fs.readFileSync(file, 'utf-8'));
31
+ }
32
+ catch {
33
+ // A malformed state file is treated as absent rather than fatal — the
34
+ // remedy is `flo swarm init`, which overwrites it.
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+ function writeSwarmState(state) {
40
+ const [canonical] = swarmStateFileCandidates();
41
+ fs.mkdirSync(path.dirname(canonical), { recursive: true });
42
+ fs.writeFileSync(canonical, JSON.stringify(state, null, 2));
43
+ }
44
+ /**
45
+ * Resolve the recorded swarm, or print why it cannot be and return null —
46
+ * in which case the caller exits non-zero without doing anything.
47
+ *
48
+ * Shared by `stop` and `scale`: both must refuse an id that is not the active
49
+ * swarm rather than acting on it, which is half of what #1428 was about.
50
+ */
51
+ function resolveActiveSwarm(swarmId, missingMessage) {
52
+ const existing = readSwarmState();
53
+ if (!existing) {
54
+ output.printError(missingMessage);
55
+ output.printInfo('Run "flo swarm status" to see what is recorded for this project.');
56
+ return null;
57
+ }
58
+ if (existing.id !== swarmId) {
59
+ output.printError(`Swarm ${swarmId} is not the active swarm.`);
60
+ output.printInfo(`Active swarm is ${existing.id} — run "flo swarm status" to confirm.`);
61
+ return null;
62
+ }
63
+ return existing;
64
+ }
65
+ /** Remove both the canonical and legacy state files; returns true if any existed. */
66
+ function clearSwarmState() {
67
+ let removed = false;
68
+ for (const file of swarmStateFileCandidates()) {
69
+ if (!fs.existsSync(file))
70
+ continue;
71
+ try {
72
+ fs.rmSync(file, { force: true });
73
+ removed = true;
74
+ }
75
+ catch {
76
+ // Leave `removed` false — the caller reports the failure rather than
77
+ // claiming a stop that did not happen.
78
+ }
79
+ }
80
+ return removed;
81
+ }
13
82
  // Get dynamic swarm status from memory/session files
14
83
  function getSwarmStatus(swarmId) {
15
84
  const projectRoot = findProjectRoot();
@@ -19,24 +88,11 @@ function getSwarmStatus(swarmId) {
19
88
  // were removed — no current writer creates those directories, so they
20
89
  // always produced 0 counts. The coordinator-backed MCP tools
21
90
  // (agent_list / task_list) are the live source of truth.
22
- const canonicalSwarmDir = path.join(mofloDir(projectRoot), 'swarm');
23
- const legacySwarmDir = path.join(projectRoot, LEGACY_SWARM_DIR);
24
91
  const sessionDir = path.join(process.cwd(), '.claude', 'sessions');
25
92
  const memoryPaths = memoryDbCandidatePaths(resolveStateRoot());
26
- // Check for active swarm state file canonical first, then legacy.
27
- let swarmStateFile = path.join(canonicalSwarmDir, 'state.json');
28
- if (!fs.existsSync(swarmStateFile)) {
29
- swarmStateFile = path.join(legacySwarmDir, 'state.json');
30
- }
31
- let swarmState = null;
32
- if (fs.existsSync(swarmStateFile)) {
33
- try {
34
- swarmState = JSON.parse(fs.readFileSync(swarmStateFile, 'utf-8'));
35
- }
36
- catch {
37
- // Ignore parse errors
38
- }
39
- }
93
+ // Canonical first, then legacy shared with init/start/stop so all four
94
+ // subcommands resolve the same file.
95
+ const swarmState = readSwarmState();
40
96
  // agents/tasks counters: no file-store readers post-#1168. Coordinator
41
97
  // MCP tools own the live counts; getSwarmStatus surfaces a static summary
42
98
  // of the persisted state file plus session/memory rough indicators.
@@ -88,7 +144,10 @@ function getSwarmStatus(swarmId) {
88
144
  status = 'completed';
89
145
  }
90
146
  else if (swarmState) {
91
- status = 'ready';
147
+ // Honour what was recorded. `start` writes `running`; `init` writes
148
+ // `ready`. Pre-#1428 this collapsed both to `ready`, so a recorded
149
+ // objective was invisible here.
150
+ status = swarmState.status || 'ready';
92
151
  }
93
152
  return {
94
153
  id: swarmId || swarmState?.id || 'no-active-swarm',
@@ -249,14 +308,9 @@ const initCommand = {
249
308
  // Save swarm state locally for status command to read. Post-#1168 the
250
309
  // canonical home is `<root>/.moflo/swarm/state.json`; the legacy
251
310
  // `.swarm/state.json` path is preserved as a read-only fallback in
252
- // `getSwarmStatus`.
253
- const swarmDir = path.join(mofloDir(findProjectRoot()), 'swarm');
311
+ // `readSwarmState`.
254
312
  try {
255
- if (!fs.existsSync(swarmDir)) {
256
- fs.mkdirSync(swarmDir, { recursive: true });
257
- }
258
- const stateFile = path.join(swarmDir, 'state.json');
259
- fs.writeFileSync(stateFile, JSON.stringify({
313
+ writeSwarmState({
260
314
  id: result.swarmId,
261
315
  topology: result.topology,
262
316
  maxAgents: result.config.maxAgents,
@@ -264,7 +318,7 @@ const initCommand = {
264
318
  v3Mode,
265
319
  initializedAt: result.initializedAt,
266
320
  status: 'ready'
267
- }, null, 2));
321
+ });
268
322
  }
269
323
  catch {
270
324
  // Ignore errors writing state file
@@ -338,12 +392,22 @@ const startCommand = {
338
392
  });
339
393
  }
340
394
  strategy = strategy || 'development';
395
+ // #1428 — refuse rather than narrate. Pre-fix this command printed "All
396
+ // agents deployed" and a `flo swarm status <id>` follow-up for an id it
397
+ // never persisted, so the advertised command reported either no active
398
+ // swarm or the state of an unrelated earlier `init`.
399
+ const existing = readSwarmState();
400
+ if (!existing) {
401
+ output.printError('No initialised swarm found.');
402
+ output.printInfo('Run "flo swarm init" first — it records the topology this objective attaches to.');
403
+ return { success: false, exitCode: 1 };
404
+ }
341
405
  output.writeln();
342
406
  output.printInfo(`Starting swarm with objective: ${output.highlight(objective)}`);
343
407
  output.writeln();
344
408
  // Compute agent deployment plan based on strategy
345
409
  const agentPlan = getAgentPlan(strategy);
346
- output.writeln(output.bold('Agent Deployment Plan'));
410
+ output.writeln(output.bold('Planned Agent Roster'));
347
411
  output.printTable({
348
412
  columns: [
349
413
  { key: 'role', header: 'Role', width: 20 },
@@ -353,10 +417,11 @@ const startCommand = {
353
417
  ],
354
418
  data: agentPlan
355
419
  });
420
+ const plannedAgents = agentPlan.reduce((sum, a) => sum + a.count, 0);
356
421
  // Confirm execution
357
422
  if (ctx.interactive) {
358
423
  const confirmed = await confirm({
359
- message: `Deploy ${agentPlan.reduce((sum, a) => sum + a.count, 0)} agents?`,
424
+ message: `Record this objective for ${plannedAgents} agents?`,
360
425
  default: true
361
426
  });
362
427
  if (!confirmed) {
@@ -364,26 +429,47 @@ const startCommand = {
364
429
  return { success: true };
365
430
  }
366
431
  }
367
- output.writeln();
368
- output.printInfo('Deploying agents...');
369
- // Show deployment progress
370
- const spinner = output.createSpinner({ text: 'Initializing agents...', spinner: 'dots' });
371
- spinner.start();
372
- // Brief delay for spinner animation
373
- await new Promise(resolve => setTimeout(resolve, 500));
374
- spinner.succeed('All agents deployed');
432
+ const startedAt = new Date().toISOString();
375
433
  const executionState = {
376
- swarmId: `swarm-${Date.now().toString(36)}`,
434
+ // The persisted id, never a freshly-minted one — `flo swarm status`
435
+ // resolves ids out of this same file, so a local mint was guaranteed
436
+ // to be unresolvable.
437
+ swarmId: existing.id,
377
438
  objective,
378
439
  strategy,
379
440
  status: 'running',
380
- agents: agentPlan.reduce((sum, a) => sum + a.count, 0),
381
- startedAt: new Date().toISOString(),
441
+ agents: plannedAgents,
442
+ startedAt,
382
443
  parallel: ctx.flags.parallel ?? true
383
444
  };
445
+ try {
446
+ writeSwarmState({
447
+ ...existing,
448
+ strategy,
449
+ objective,
450
+ plannedAgents,
451
+ startedAt,
452
+ status: 'running',
453
+ });
454
+ }
455
+ catch (error) {
456
+ output.printError(`Failed to record objective: ${String(error)}`);
457
+ return { success: false, exitCode: 1 };
458
+ }
384
459
  output.writeln();
385
- output.printSuccess('Swarm execution started');
460
+ output.printSuccess(`Objective recorded against swarm ${executionState.swarmId}`);
386
461
  output.writeln(output.dim(` Monitor: flo swarm status ${executionState.swarmId}`));
462
+ output.writeln();
463
+ // Say plainly what did NOT happen. The CLI reaches its MCP tools in-process
464
+ // (see SwarmStateFile above), so agents spawned here would not outlive this
465
+ // command — the long-lived coordinator is the one inside the MCP server.
466
+ output.printInfo('No agents were spawned by this command.');
467
+ output.writeln(output.dim(' The roster above is a plan. Agents run on the long-lived coordinator'));
468
+ output.writeln(output.dim(' in the MCP server — drive them from a Claude Code session with the'));
469
+ output.writeln(output.dim(' swarm_init / agent_spawn / task_orchestrate tools.'));
470
+ if (ctx.flags.format === 'json') {
471
+ output.printJson(executionState);
472
+ }
387
473
  return { success: true, data: executionState };
388
474
  }
389
475
  };
@@ -491,9 +577,16 @@ const stopCommand = {
491
577
  output.printInfo('Run "moflo swarm status" to find the active swarm ID');
492
578
  return { success: false, exitCode: 1 };
493
579
  }
580
+ // #1428 — `stop` used to print "Swarm <id> stopped" for any string at all,
581
+ // including an id that never existed, while leaving the state file in
582
+ // place. It is the counterpart to `start`: if it does not clear that file,
583
+ // `flo swarm status` reports the objective as running indefinitely.
584
+ const existing = resolveActiveSwarm(swarmId, 'No active swarm to stop.');
585
+ if (!existing)
586
+ return { success: false, exitCode: 1 };
494
587
  if (ctx.interactive && !force) {
495
588
  const confirmed = await confirm({
496
- message: `Stop swarm ${swarmId}? Progress will be saved.`,
589
+ message: `Stop swarm ${swarmId}? Its recorded objective will be cleared.`,
497
590
  default: false
498
591
  });
499
592
  if (!confirmed) {
@@ -502,13 +595,14 @@ const stopCommand = {
502
595
  }
503
596
  }
504
597
  output.printInfo(`Stopping swarm ${swarmId}...`);
505
- if (!force) {
506
- output.writeln(output.dim(' Completing in-progress tasks...'));
507
- output.writeln(output.dim(' Saving coordination state...'));
508
- output.writeln(output.dim(' Notifying agents...'));
509
- output.writeln(output.dim(' Saving memory state...'));
598
+ if (!clearSwarmState()) {
599
+ output.printError(`Failed to clear swarm state for ${swarmId}.`);
600
+ return { success: false, exitCode: 1 };
510
601
  }
511
602
  output.printSuccess(`Swarm ${swarmId} stopped`);
603
+ // Only the recorded state is ours to clear — see SwarmStateFile.
604
+ output.writeln(output.dim(' Cleared the recorded swarm state. Agents running on the MCP'));
605
+ output.writeln(output.dim(' coordinator are unaffected — terminate those with agent_terminate.'));
512
606
  return { success: true, data: { swarmId, stopped: true, force } };
513
607
  }
514
608
  };
@@ -544,88 +638,106 @@ const scaleCommand = {
544
638
  output.printError('Target agent count required. Use --agents or -a');
545
639
  return { success: false, exitCode: 1 };
546
640
  }
547
- output.printInfo(`Scaling swarm ${swarmId} to ${targetAgents} agents...`);
548
- // Calculate scaling delta
549
- const currentAgents = 8;
550
- const delta = targetAgents - currentAgents;
551
- if (delta > 0) {
552
- output.writeln(output.dim(` Spawning ${delta} new agents...`));
641
+ // #1428 — same treatment as `start`, for the same structural reason. The
642
+ // wired `swarm_scale` MCP tool really does call coordinator.spawnAgent /
643
+ // terminateAgent, but calling it from here would drive a coordinator that
644
+ // dies with this process: the agents would be spawned and lost between one
645
+ // command and the next. Pre-fix this computed a delta against a hardcoded
646
+ // `currentAgents = 8` and announced "Swarm scaled to N agents".
647
+ const existing = resolveActiveSwarm(swarmId, 'No initialised swarm found.');
648
+ if (!existing)
649
+ return { success: false, exitCode: 1 };
650
+ // The recorded ceiling, which is a real number `init` wrote — not a guess.
651
+ const previous = existing.maxAgents;
652
+ if (previous === targetAgents) {
653
+ output.printInfo(`Recorded agent ceiling is already ${targetAgents}.`);
654
+ return { success: true, data: { swarmId, agents: targetAgents, changed: false } };
655
+ }
656
+ try {
657
+ writeSwarmState({ ...existing, maxAgents: targetAgents });
553
658
  }
554
- else if (delta < 0) {
555
- output.writeln(output.dim(` Gracefully stopping ${-delta} agents...`));
659
+ catch (error) {
660
+ output.printError(`Failed to record agent ceiling: ${String(error)}`);
661
+ return { success: false, exitCode: 1 };
556
662
  }
557
- else {
558
- output.printInfo('Swarm already at target size');
559
- return { success: true };
663
+ output.printSuccess(`Recorded agent ceiling for ${swarmId}: ${previous ?? 'unset'} → ${targetAgents}`);
664
+ output.writeln();
665
+ output.printInfo('No agents were spawned or terminated by this command.');
666
+ output.writeln(output.dim(' This records the ceiling a later swarm_init will use. To scale a'));
667
+ output.writeln(output.dim(' running swarm, call the swarm_scale MCP tool from a Claude Code'));
668
+ output.writeln(output.dim(' session — that reaches the live coordinator, which this CLI cannot.'));
669
+ if (agentType) {
670
+ output.writeln(output.dim(` --type ${agentType} applies only to live scaling via swarm_scale.`));
560
671
  }
561
- output.printSuccess(`Swarm scaled to ${targetAgents} agents`);
562
- return { success: true, data: { swarmId, agents: targetAgents, delta } };
672
+ return {
673
+ success: true,
674
+ data: { swarmId, agents: targetAgents, previousAgents: previous, changed: true },
675
+ };
563
676
  }
564
677
  };
565
- // Coordinate command (V3 specific)
678
+ /**
679
+ * The V3 roster `--v3-mode` is designed around. This is reference data, not
680
+ * observed state: no agent here is running, and nothing reads it back.
681
+ *
682
+ * #1428 — it used to carry a per-agent `status` of `primary`/`active`/
683
+ * `standby`, rendered in colour, which read as live telemetry for agents that
684
+ * did not exist. The roster itself is real (the topology it describes is what
685
+ * `swarm init --v3-mode` actually selects), so the fix is to drop the invented
686
+ * status column rather than the table.
687
+ */
688
+ const V3_ROSTER = [
689
+ { id: 1, role: 'Queen Coordinator', domain: 'Orchestration' },
690
+ { id: 2, role: 'Security Architect', domain: 'Security' },
691
+ { id: 3, role: 'Security Auditor', domain: 'Security' },
692
+ { id: 4, role: 'Test Architect', domain: 'Security' },
693
+ { id: 5, role: 'Core Architect', domain: 'Core' },
694
+ { id: 6, role: 'Memory Specialist', domain: 'Core' },
695
+ { id: 7, role: 'Swarm Specialist', domain: 'Core' },
696
+ { id: 8, role: 'Integration Architect', domain: 'Integration' },
697
+ { id: 9, role: 'Performance Engineer', domain: 'Integration' },
698
+ { id: 10, role: 'CLI Developer', domain: 'Integration' },
699
+ { id: 11, role: 'Hooks Developer', domain: 'Integration' },
700
+ { id: 12, role: 'MCP Specialist', domain: 'Integration' },
701
+ { id: 13, role: 'Project Coordinator', domain: 'Management' },
702
+ { id: 14, role: 'Documentation Lead', domain: 'Management' },
703
+ { id: 15, role: 'DevOps Engineer', domain: 'Management' },
704
+ ];
705
+ // Reference display for the V3 roster. Named `coordinate` for backward
706
+ // compatibility — it coordinates nothing, and no longer claims to.
566
707
  const coordinateCommand = {
567
708
  name: 'coordinate',
568
- description: 'Execute V3 15-agent hierarchical mesh coordination',
709
+ description: 'Show the V3 15-agent hierarchical-mesh roster (reference only — starts nothing)',
569
710
  options: [
570
711
  {
571
712
  name: 'agents',
572
- description: 'Number of agents',
713
+ description: 'Number of roster entries to show',
573
714
  type: 'number',
574
715
  default: 15
575
- },
576
- {
577
- name: 'domains',
578
- description: 'Domains to activate',
579
- type: 'array'
580
716
  }
581
717
  ],
582
718
  action: async (ctx) => {
583
719
  const agentCount = ctx.flags.agents || 15;
720
+ const roster = V3_ROSTER.slice(0, agentCount);
584
721
  output.writeln();
585
- output.writeln(output.bold('V3 15-Agent Hierarchical Mesh Coordination'));
722
+ output.writeln(output.bold('V3 Hierarchical-Mesh Roster (reference)'));
723
+ output.writeln(output.dim(' The layout `flo swarm init --v3-mode` is designed around.'));
586
724
  output.writeln();
587
- // V3 agent structure
588
- const v3Agents = [
589
- { id: 1, role: 'Queen Coordinator', domain: 'Orchestration', status: 'primary' },
590
- { id: 2, role: 'Security Architect', domain: 'Security', status: 'active' },
591
- { id: 3, role: 'Security Auditor', domain: 'Security', status: 'active' },
592
- { id: 4, role: 'Test Architect', domain: 'Security', status: 'active' },
593
- { id: 5, role: 'Core Architect', domain: 'Core', status: 'active' },
594
- { id: 6, role: 'Memory Specialist', domain: 'Core', status: 'active' },
595
- { id: 7, role: 'Swarm Specialist', domain: 'Core', status: 'active' },
596
- { id: 8, role: 'Integration Architect', domain: 'Integration', status: 'active' },
597
- { id: 9, role: 'Performance Engineer', domain: 'Integration', status: 'active' },
598
- { id: 10, role: 'CLI Developer', domain: 'Integration', status: 'active' },
599
- { id: 11, role: 'Hooks Developer', domain: 'Integration', status: 'active' },
600
- { id: 12, role: 'MCP Specialist', domain: 'Integration', status: 'active' },
601
- { id: 13, role: 'Project Coordinator', domain: 'Management', status: 'active' },
602
- { id: 14, role: 'Documentation Lead', domain: 'Management', status: 'standby' },
603
- { id: 15, role: 'DevOps Engineer', domain: 'Management', status: 'standby' }
604
- ].slice(0, agentCount);
605
725
  output.printTable({
606
726
  columns: [
607
727
  { key: 'id', header: '#', width: 3, align: 'right' },
608
728
  { key: 'role', header: 'Role', width: 22 },
609
- { key: 'domain', header: 'Domain', width: 15 },
610
- { key: 'status', header: 'Status', width: 10, format: (v) => {
611
- if (v === 'primary')
612
- return output.highlight(String(v));
613
- if (v === 'active')
614
- return output.success(String(v));
615
- return output.dim(String(v));
616
- } }
729
+ { key: 'domain', header: 'Domain', width: 15 }
617
730
  ],
618
- data: v3Agents
731
+ data: roster
619
732
  });
620
733
  output.writeln();
621
- output.printInfo('Capabilities:');
622
- output.printList([
623
- `Flash Attention: ${output.success('memory-efficient attention')}`,
624
- `AgentDB Search: ${output.success('HNSW (ANN)')}`,
625
- `Memory: ${output.success('Int8 quantized')}`,
626
- `Code Reduction: ${output.success('<5,000 lines')}`
627
- ]);
628
- return { success: true, data: { agents: v3Agents, count: agentCount } };
734
+ output.printInfo('No agents were started, and none are running as a result of this command.');
735
+ output.writeln(output.dim(' Initialise the topology with: flo swarm init --v3-mode'));
736
+ output.writeln(output.dim(' Spawn real agents from a Claude Code session with agent_spawn.'));
737
+ if (ctx.flags.format === 'json') {
738
+ output.printJson({ roster, count: roster.length });
739
+ }
740
+ return { success: true, data: { agents: roster, count: roster.length } };
629
741
  }
630
742
  };
631
743
  // Main swarm command
@@ -637,7 +749,7 @@ export const swarmCommand = {
637
749
  examples: [
638
750
  { command: 'flo swarm init --v3-mode', description: 'Initialize V3 swarm' },
639
751
  { command: 'flo swarm start -o "Build API" -s development', description: 'Start development swarm' },
640
- { command: 'flo swarm coordinate --agents 15', description: 'V3 coordination' }
752
+ { command: 'flo swarm coordinate --agents 15', description: 'Show the V3 roster (reference)' }
641
753
  ],
642
754
  action: async (ctx) => {
643
755
  output.writeln();
@@ -652,7 +764,7 @@ export const swarmCommand = {
652
764
  `${output.highlight('status')} - Show swarm status`,
653
765
  `${output.highlight('stop')} - Stop swarm execution`,
654
766
  `${output.highlight('scale')} - Scale swarm agent count`,
655
- `${output.highlight('coordinate')} - V3 15-agent coordination`
767
+ `${output.highlight('coordinate')} - Show the V3 15-agent roster (reference)`
656
768
  ]);
657
769
  return { success: true };
658
770
  }
@@ -13,6 +13,7 @@
13
13
  *
14
14
  * @module moflo/cli/guidance/headless
15
15
  */
16
+ import { escapeShellArg, NULL_DEVICE } from '../shared/utils/platform.js';
16
17
  /**
17
18
  * Default command executor using child_process
18
19
  */
@@ -162,14 +163,10 @@ export class HeadlessRunner {
162
163
  * Build the Claude Code headless command
163
164
  */
164
165
  buildCommand(task) {
165
- // Escape the prompt for shell safety
166
- const nd = process.platform === 'win32' ? 'NUL' : '/dev/null';
167
- if (process.platform === 'win32') {
168
- const escapedPrompt = task.prompt.replace(/"/g, '\\"');
169
- return `claude -p "${escapedPrompt}" --output-format json 2>${nd}`;
170
- }
171
- const escapedPrompt = task.prompt.replace(/'/g, "'\\''");
172
- return `claude -p '${escapedPrompt}' --output-format json 2>${nd}`;
166
+ // Quoting and the null device both come from platform.ts — this used to
167
+ // branch on process.platform twice and hand-roll the Windows escape, which
168
+ // is where the trailing-backslash defect lived (#1419).
169
+ return `claude -p ${escapeShellArg(task.prompt)} --output-format json 2>${NULL_DEVICE}`;
173
170
  }
174
171
  /**
175
172
  * Parse Claude Code JSON output
@@ -303,12 +303,7 @@ export class ShardRetriever {
303
303
  matchGlob(path, glob) {
304
304
  let re = this.globCache.get(glob);
305
305
  if (!re) {
306
- const pattern = glob
307
- .replace(/\*\*/g, '{{GLOBSTAR}}')
308
- .replace(/\*/g, '[^/]*')
309
- .replace(/{{GLOBSTAR}}/g, '.*')
310
- .replace(/\//g, '\\/');
311
- re = new RegExp(`^${pattern}$`);
306
+ re = globToRegExp(glob);
312
307
  this.globCache.set(glob, re);
313
308
  }
314
309
  return re.test(path);
@@ -348,4 +343,30 @@ export class ShardRetriever {
348
343
  export function createRetriever(embeddingProvider) {
349
344
  return new ShardRetriever(embeddingProvider);
350
345
  }
346
+ /**
347
+ * Compile a repo-scope glob (`*`, `**`) into an anchored regex.
348
+ *
349
+ * Metacharacters are escaped FIRST, then the wildcards are substituted. Without
350
+ * the escape a glob containing `.`, `+`, `(` or `[` either matched the wrong
351
+ * thing — `docs/*.md` happily matching `docsXmd`, because the `.` was a
352
+ * wildcard — or threw from `new RegExp` on an unbalanced bracket. Guidance
353
+ * scopes carry `.md` routinely, so the silent-mismatch case was the live one
354
+ * (#1419).
355
+ *
356
+ * `*` is left unescaped so the substitutions can still see it. The
357
+ * `{{GLOBSTAR}}` placeholder cannot collide with a literal `{{GLOBSTAR}}` in the
358
+ * input because that input has already had its braces escaped to `\{\{…\}\}` by
359
+ * the time the placeholder is inserted.
360
+ *
361
+ * Exported for direct testing: the only caller is a private method reachable
362
+ * solely through a fully-loaded retriever.
363
+ */
364
+ export function globToRegExp(glob) {
365
+ const pattern = glob
366
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&')
367
+ .replace(/\*\*/g, '{{GLOBSTAR}}')
368
+ .replace(/\*/g, '[^/]*')
369
+ .replace(/\{\{GLOBSTAR\}\}/g, '.*');
370
+ return new RegExp(`^${pattern}$`);
371
+ }
351
372
  //# sourceMappingURL=retriever.js.map
@@ -16,6 +16,7 @@
16
16
  *
17
17
  * @module moflo/cli/guidance/ruvbot-integration
18
18
  */
19
+ import { generateId } from '../shared/utils/id.js';
19
20
  // ============================================================================
20
21
  // Severity Mapping
21
22
  // ============================================================================
@@ -587,7 +588,7 @@ export class RuvBotGuidanceBridge {
587
588
  */
588
589
  async handleSessionCreate(...args) {
589
590
  const data = (args[0] ?? {});
590
- const sessionId = String(data['sessionId'] ?? data['id'] ?? `session-${Date.now()}`);
591
+ const sessionId = String(data['sessionId'] ?? data['id'] ?? generateId('session'));
591
592
  this.logEvent('session:create', { sessionId });
592
593
  if (this.config.enableProofChain) {
593
594
  if (!this.config.proofSigningKey) {
@@ -24,6 +24,7 @@
24
24
  * @module moflo/cli/guidance/uncertainty
25
25
  */
26
26
  import { randomUUID } from 'node:crypto';
27
+ import { generateId } from '../shared/utils/id.js';
27
28
  // ============================================================================
28
29
  // Default Configuration
29
30
  // ============================================================================
@@ -294,7 +295,7 @@ export class UncertaintyLedger {
294
295
  }
295
296
  // Record the resolution as evidence
296
297
  const resolutionEvidence = {
297
- sourceId: `resolution:${beliefId}:${Date.now()}`,
298
+ sourceId: generateId(`resolution:${beliefId}`, { separator: ':' }),
298
299
  sourceType: 'human-input',
299
300
  supports: status === 'confirmed',
300
301
  weight: 1.0,
@@ -8,6 +8,7 @@
8
8
  * @module v3/hooks/bridge/official-hooks-bridge
9
9
  */
10
10
  import { HookEvent } from '../types.js';
11
+ import { generateId } from '../../shared/utils/id.js';
11
12
  /**
12
13
  * Mapping from V3 HookEvent to Official hook events
13
14
  */
@@ -100,7 +101,7 @@ export class OfficialHooksBridge {
100
101
  // Add task information for Task tool
101
102
  if (input.tool_name === 'Task') {
102
103
  context.task = {
103
- id: `task-${Date.now()}`,
104
+ id: generateId('task'),
104
105
  description: input.tool_input?.prompt ?? '',
105
106
  agent: input.tool_input?.subagent_type,
106
107
  };
@@ -4,6 +4,7 @@
4
4
  * MCP tool definitions for hooks system integration.
5
5
  * These tools provide programmatic access to hooks functionality.
6
6
  */
7
+ import { generateId } from '../../shared/utils/id.js';
7
8
  /**
8
9
  * Pre-edit hook MCP tool
9
10
  */
@@ -110,7 +111,7 @@ export const postEditTool = {
110
111
  success,
111
112
  recorded: true,
112
113
  recordedAt: new Date().toISOString(),
113
- patternId: success ? `pattern-${Date.now()}` : undefined,
114
+ patternId: success ? generateId('pattern') : undefined,
114
115
  };
115
116
  },
116
117
  };
@@ -314,7 +315,7 @@ export const postCommandTool = {
314
315
  const success = input.success;
315
316
  return {
316
317
  recorded: true,
317
- patternId: success ? `cmd-${Date.now()}` : undefined,
318
+ patternId: success ? generateId('cmd') : undefined,
318
319
  };
319
320
  },
320
321
  };
@@ -15,6 +15,7 @@
15
15
  import { EventEmitter } from 'node:events';
16
16
  import { TestDeterministicEmbedding } from './__mocks__/test-embedding-service.js';
17
17
  import { MEMORY_DB_FILE, MOFLO_DIR } from '../../services/moflo-paths.js';
18
+ import { generateId } from '../../shared/utils/id.js';
18
19
  // Dynamic imports for optional dependencies
19
20
  let MofloDbAdapter = null;
20
21
  let HNSWIndex = null;
@@ -244,7 +245,7 @@ export class ReasoningBank extends EventEmitter {
244
245
  }
245
246
  // Create new pattern
246
247
  const pattern = {
247
- id: `pat_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
248
+ id: generateId('pat', { separator: '_' }),
248
249
  strategy,
249
250
  domain,
250
251
  embedding,
@@ -4,6 +4,7 @@
4
4
  * Central registry for hook registration, management, and lookup.
5
5
  * Provides priority-based execution ordering and filtering.
6
6
  */
7
+ import { generateId } from '../../shared/utils/id.js';
7
8
  /**
8
9
  * Hook Registry - manages hook registration and lookup
9
10
  */
@@ -189,7 +190,7 @@ export class HookRegistry {
189
190
  * Generate unique hook ID
190
191
  */
191
192
  generateId() {
192
- return `hook-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
193
+ return generateId('hook');
193
194
  }
194
195
  }
195
196
  /**