moflo 4.9.21 → 4.9.23

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 (194) hide show
  1. package/.claude/agents/analysis/analyze-code-quality.md +0 -121
  2. package/.claude/agents/analysis/code-analyzer.md +5 -26
  3. package/.claude/agents/architecture/system-design/arch-system-design.md +0 -119
  4. package/.claude/agents/base-template-generator.md +0 -1
  5. package/.claude/agents/core/coder.md +0 -22
  6. package/.claude/agents/core/planner.md +0 -16
  7. package/.claude/agents/core/researcher.md +0 -16
  8. package/.claude/agents/core/reviewer.md +0 -17
  9. package/.claude/agents/core/tester.md +0 -19
  10. package/.claude/agents/custom/test-long-runner.md +0 -2
  11. package/.claude/agents/development/dev-backend-api.md +0 -167
  12. package/.claude/agents/development/dev-database.md +43 -0
  13. package/.claude/agents/development/dev-frontend.md +42 -0
  14. package/.claude/agents/devops/ci-cd/ops-cicd-github.md +0 -112
  15. package/.claude/agents/documentation/api-docs/docs-api-openapi.md +0 -111
  16. package/.claude/agents/security/security-auditor.md +45 -0
  17. package/.claude/guidance/shipped/moflo-cli-reference.md +19 -16
  18. package/.claude/guidance/shipped/moflo-core-guidance.md +0 -2
  19. package/.claude/guidance/shipped/moflo-guidance-rules.md +5 -5
  20. package/.claude/guidance/shipped/moflo-spell-runner.md +1 -0
  21. package/.claude/guidance/shipped/moflo-spell-scheduling.md +225 -0
  22. package/.claude/guidance/shipped/moflo-spell-troubleshooting.md +1 -0
  23. package/.claude/helpers/gate.cjs +70 -3
  24. package/.claude/skills/fl/execution-modes.md +38 -15
  25. package/.claude/skills/fl/phases.md +67 -0
  26. package/.claude/skills/spell-schedule/SKILL.md +18 -5
  27. package/README.md +1 -1
  28. package/bin/gate.cjs +70 -3
  29. package/bin/index-guidance.mjs +32 -6
  30. package/bin/lib/retired-files.mjs +146 -0
  31. package/bin/session-start-launcher.mjs +116 -8
  32. package/dist/src/cli/appliance/rvfa-builder.js +1 -1
  33. package/dist/src/cli/commands/agent.js +3 -9
  34. package/dist/src/cli/commands/daemon.js +13 -17
  35. package/dist/src/cli/commands/hooks.js +4 -9
  36. package/dist/src/cli/commands/index.js +2 -0
  37. package/dist/src/cli/commands/retire.js +111 -0
  38. package/dist/src/cli/commands/spell-schedule.js +237 -49
  39. package/dist/src/cli/hooks/reasoningbank/index.js +7 -7
  40. package/dist/src/cli/init/executor.js +26 -54
  41. package/dist/src/cli/init/helpers-generator.js +66 -3
  42. package/dist/src/cli/init/settings-generator.js +17 -6
  43. package/dist/src/cli/mcp-tools/agent-tools.js +9 -27
  44. package/dist/src/cli/mcp-tools/hooks-tools.js +23 -21
  45. package/dist/src/cli/mcp-tools/memory-tools.js +16 -5
  46. package/dist/src/cli/memory/bridge-embedder.js +26 -6
  47. package/dist/src/cli/memory/bridge-entries.js +33 -15
  48. package/dist/src/cli/memory/controllers/semantic-router.js +18 -12
  49. package/dist/src/cli/memory/sona-optimizer.js +6 -6
  50. package/dist/src/cli/neural/domain/services/learning-service.js +3 -3
  51. package/dist/src/cli/services/agent-router.js +2 -5
  52. package/dist/src/cli/services/daemon-autostart-lifecycle.js +62 -0
  53. package/dist/src/cli/services/daemon-dashboard.js +187 -18
  54. package/dist/src/cli/services/daemon-readiness.js +19 -31
  55. package/dist/src/cli/services/ephemeral-namespace-purge.js +61 -33
  56. package/dist/src/cli/services/headless-worker-executor.js +7 -94
  57. package/dist/src/cli/services/hook-block-hash.js +4 -0
  58. package/dist/src/cli/services/worker-daemon.js +40 -66
  59. package/dist/src/cli/shared/events/example-usage.js +6 -6
  60. package/dist/src/cli/shared/hooks/task-hooks.js +8 -8
  61. package/dist/src/cli/spells/core/runner.js +12 -0
  62. package/dist/src/cli/spells/scheduler/scheduler.js +24 -9
  63. package/dist/src/cli/spells/schema/validator.js +2 -1
  64. package/dist/src/cli/spells/schema/validators/top-level.js +18 -0
  65. package/dist/src/cli/version.js +1 -1
  66. package/package.json +5 -2
  67. package/retired-files.json +1989 -0
  68. package/src/cli/data/model-registry.json +2 -2
  69. package/.claude/agents/consensus/byzantine-coordinator.md +0 -63
  70. package/.claude/agents/consensus/crdt-synchronizer.md +0 -997
  71. package/.claude/agents/consensus/gossip-coordinator.md +0 -63
  72. package/.claude/agents/consensus/performance-benchmarker.md +0 -851
  73. package/.claude/agents/consensus/quorum-manager.md +0 -823
  74. package/.claude/agents/consensus/raft-manager.md +0 -63
  75. package/.claude/agents/consensus/security-manager.md +0 -622
  76. package/.claude/agents/data/ml/data-ml-model.md +0 -193
  77. package/.claude/agents/github/code-review-swarm.md +0 -538
  78. package/.claude/agents/github/github-modes.md +0 -172
  79. package/.claude/agents/github/issue-tracker.md +0 -311
  80. package/.claude/agents/github/multi-repo-swarm.md +0 -551
  81. package/.claude/agents/github/pr-manager.md +0 -183
  82. package/.claude/agents/github/project-board-sync.md +0 -508
  83. package/.claude/agents/github/release-manager.md +0 -360
  84. package/.claude/agents/github/release-swarm.md +0 -580
  85. package/.claude/agents/github/repo-architect.md +0 -391
  86. package/.claude/agents/github/swarm-issue.md +0 -566
  87. package/.claude/agents/github/swarm-pr.md +0 -414
  88. package/.claude/agents/github/sync-coordinator.md +0 -426
  89. package/.claude/agents/github/workflow-automation.md +0 -606
  90. package/.claude/agents/goal/code-goal-planner.md +0 -440
  91. package/.claude/agents/goal/goal-planner.md +0 -168
  92. package/.claude/agents/hive-mind/collective-intelligence-coordinator.md +0 -127
  93. package/.claude/agents/hive-mind/queen-coordinator.md +0 -198
  94. package/.claude/agents/hive-mind/scout-explorer.md +0 -233
  95. package/.claude/agents/hive-mind/swarm-memory-manager.md +0 -184
  96. package/.claude/agents/hive-mind/worker-specialist.md +0 -208
  97. package/.claude/agents/neural/safla-neural.md +0 -73
  98. package/.claude/agents/optimization/benchmark-suite.md +0 -665
  99. package/.claude/agents/optimization/load-balancer.md +0 -431
  100. package/.claude/agents/optimization/performance-monitor.md +0 -672
  101. package/.claude/agents/optimization/resource-allocator.md +0 -674
  102. package/.claude/agents/optimization/topology-optimizer.md +0 -808
  103. package/.claude/agents/reasoning/goal-planner.md +0 -67
  104. package/.claude/agents/sona/sona-learning-optimizer.md +0 -74
  105. package/.claude/agents/sparc/architecture.md +0 -472
  106. package/.claude/agents/sparc/pseudocode.md +0 -318
  107. package/.claude/agents/sparc/refinement.md +0 -525
  108. package/.claude/agents/sparc/specification.md +0 -276
  109. package/.claude/agents/specialized/mobile/spec-mobile-react-native.md +0 -225
  110. package/.claude/agents/swarm/adaptive-coordinator.md +0 -391
  111. package/.claude/agents/swarm/hierarchical-coordinator.md +0 -321
  112. package/.claude/agents/swarm/mesh-coordinator.md +0 -383
  113. package/.claude/agents/testing/production-validator.md +0 -395
  114. package/.claude/agents/testing/tdd-london-swarm.md +0 -244
  115. package/.claude/agents/v3/adr-architect.md +0 -184
  116. package/.claude/agents/v3/aidefence-guardian.md +0 -277
  117. package/.claude/agents/v3/claims-authorizer.md +0 -208
  118. package/.claude/agents/v3/collective-intelligence-coordinator.md +0 -988
  119. package/.claude/agents/v3/ddd-domain-expert.md +0 -220
  120. package/.claude/agents/v3/injection-analyst.md +0 -232
  121. package/.claude/agents/v3/memory-specialist.md +0 -987
  122. package/.claude/agents/v3/performance-engineer.md +0 -1225
  123. package/.claude/agents/v3/pii-detector.md +0 -146
  124. package/.claude/agents/v3/reasoningbank-learner.md +0 -213
  125. package/.claude/agents/v3/security-architect-aidefence.md +0 -405
  126. package/.claude/agents/v3/security-architect.md +0 -865
  127. package/.claude/agents/v3/security-auditor.md +0 -771
  128. package/.claude/agents/v3/sparc-orchestrator.md +0 -182
  129. package/.claude/agents/v3/swarm-memory-manager.md +0 -142
  130. package/.claude/agents/v3/v3-integration-architect.md +0 -205
  131. package/.claude/commands/claude-flow-help.md +0 -103
  132. package/.claude/commands/claude-flow-memory.md +0 -107
  133. package/.claude/commands/claude-flow-swarm.md +0 -205
  134. package/.claude/commands/flo-simplify.md +0 -101
  135. package/.claude/commands/github/README.md +0 -11
  136. package/.claude/commands/github/code-review-swarm.md +0 -514
  137. package/.claude/commands/github/code-review.md +0 -25
  138. package/.claude/commands/github/github-modes.md +0 -146
  139. package/.claude/commands/github/github-swarm.md +0 -113
  140. package/.claude/commands/github/issue-tracker.md +0 -284
  141. package/.claude/commands/github/issue-triage.md +0 -25
  142. package/.claude/commands/github/multi-repo-swarm.md +0 -519
  143. package/.claude/commands/github/pr-enhance.md +0 -26
  144. package/.claude/commands/github/pr-manager.md +0 -164
  145. package/.claude/commands/github/project-board-sync.md +0 -471
  146. package/.claude/commands/github/release-manager.md +0 -332
  147. package/.claude/commands/github/release-swarm.md +0 -544
  148. package/.claude/commands/github/repo-analyze.md +0 -25
  149. package/.claude/commands/github/repo-architect.md +0 -361
  150. package/.claude/commands/github/swarm-issue.md +0 -482
  151. package/.claude/commands/github/swarm-pr.md +0 -285
  152. package/.claude/commands/github/sync-coordinator.md +0 -294
  153. package/.claude/commands/github/workflow-automation.md +0 -442
  154. package/.claude/commands/hooks/README.md +0 -11
  155. package/.claude/commands/hooks/overview.md +0 -58
  156. package/.claude/commands/hooks/post-edit.md +0 -117
  157. package/.claude/commands/hooks/post-task.md +0 -112
  158. package/.claude/commands/hooks/pre-edit.md +0 -113
  159. package/.claude/commands/hooks/pre-task.md +0 -111
  160. package/.claude/commands/hooks/session-end.md +0 -118
  161. package/.claude/commands/hooks/setup.md +0 -103
  162. package/.claude/commands/sparc/analyzer.md +0 -42
  163. package/.claude/commands/sparc/architect.md +0 -43
  164. package/.claude/commands/sparc/ask.md +0 -86
  165. package/.claude/commands/sparc/batch-executor.md +0 -44
  166. package/.claude/commands/sparc/code.md +0 -78
  167. package/.claude/commands/sparc/coder.md +0 -44
  168. package/.claude/commands/sparc/debug.md +0 -72
  169. package/.claude/commands/sparc/debugger.md +0 -44
  170. package/.claude/commands/sparc/designer.md +0 -43
  171. package/.claude/commands/sparc/devops.md +0 -98
  172. package/.claude/commands/sparc/docs-writer.md +0 -69
  173. package/.claude/commands/sparc/documenter.md +0 -44
  174. package/.claude/commands/sparc/innovator.md +0 -44
  175. package/.claude/commands/sparc/integration.md +0 -72
  176. package/.claude/commands/sparc/mcp.md +0 -106
  177. package/.claude/commands/sparc/memory-manager.md +0 -44
  178. package/.claude/commands/sparc/optimizer.md +0 -44
  179. package/.claude/commands/sparc/orchestrator.md +0 -116
  180. package/.claude/commands/sparc/post-deployment-monitoring-mode.md +0 -72
  181. package/.claude/commands/sparc/refinement-optimization-mode.md +0 -72
  182. package/.claude/commands/sparc/researcher.md +0 -44
  183. package/.claude/commands/sparc/reviewer.md +0 -44
  184. package/.claude/commands/sparc/security-review.md +0 -69
  185. package/.claude/commands/sparc/sparc-modes.md +0 -139
  186. package/.claude/commands/sparc/sparc.md +0 -99
  187. package/.claude/commands/sparc/spec-pseudocode.md +0 -69
  188. package/.claude/commands/sparc/spell-manager.md +0 -44
  189. package/.claude/commands/sparc/supabase-admin.md +0 -337
  190. package/.claude/commands/sparc/swarm-coordinator.md +0 -44
  191. package/.claude/commands/sparc/tdd.md +0 -44
  192. package/.claude/commands/sparc/tester.md +0 -44
  193. package/.claude/commands/sparc/tutorial.md +0 -68
  194. package/.claude/commands/sparc.md +0 -151
@@ -1,8 +1,14 @@
1
1
  /**
2
- * Daemon Dashboard — Lightweight localhost HTTP server
2
+ * The Arcane Console — Lightweight localhost HTTP server
3
3
  *
4
- * Serves a read-only VanJS dashboard for daemon status, spell logs,
5
- * and memory stats. Binds to 127.0.0.1 only (no auth needed).
4
+ * Serves the moflo Arcane Console (read-only daemon view) for status,
5
+ * scheduled spells, executions, and memory stats. Binds to 127.0.0.1
6
+ * only (no auth needed).
7
+ *
8
+ * Internal/code identifiers retain the term "dashboard" (CLI flags
9
+ * `--dashboard-port` / `--no-dashboard`, internal port constant) for
10
+ * stability with existing consumer scripts; only user-facing surface
11
+ * is rebranded.
6
12
  *
7
13
  * @module daemon-dashboard
8
14
  */
@@ -78,10 +84,17 @@ function tryParseSafe(s) {
78
84
  }
79
85
  function handleStatus(daemon) {
80
86
  const status = daemon.getStatus();
87
+ // Index config rows by worker type so the row renderer can show a
88
+ // "disabled" badge instead of "Last run: never" for default-off workers
89
+ // (audit, predict, document — see #968 user feedback).
90
+ const configByType = new Map();
91
+ for (const w of status.config.workers)
92
+ configByType.set(w.type, { enabled: w.enabled });
81
93
  const workers = [];
82
94
  for (const [type, state] of status.workers) {
83
95
  workers.push({
84
96
  type,
97
+ enabled: configByType.get(type)?.enabled ?? true,
85
98
  isRunning: state.isRunning,
86
99
  lastRun: state.lastRun?.toISOString() ?? null,
87
100
  nextRun: state.nextRun?.toISOString() ?? null,
@@ -330,6 +343,9 @@ async function handleRequest(req, res, daemon, opts) {
330
343
  else if (url === '/api/schedules') {
331
344
  sendJson(res, 200, await handleSchedules(daemon, opts));
332
345
  }
346
+ else if (url === '/api/schedules/events') {
347
+ handleSchedulesEventStream(req, res, daemon);
348
+ }
333
349
  else if (url === '/api/spells') {
334
350
  sendJson(res, 200, await handleSpells(opts.memory));
335
351
  }
@@ -400,6 +416,71 @@ function getSchedulerErrorCode(err) {
400
416
  }
401
417
  return null;
402
418
  }
419
+ /** Heartbeat interval for the schedule-event SSE stream (keeps proxies + idle clients alive). */
420
+ const SSE_HEARTBEAT_MS = 25_000;
421
+ /**
422
+ * Stream `schedule:*` events to the client via Server-Sent Events.
423
+ *
424
+ * Subscribes to `scheduler.on(listener)` and forwards each event as an SSE
425
+ * frame (`event: <type>\ndata: <JSON>\n\n`). Sends an initial `ready` frame
426
+ * so the client can distinguish "connected, waiting" from "scheduler down",
427
+ * plus a comment heartbeat every 25s. Cleans up on client disconnect.
428
+ *
429
+ * Returns 503 when the scheduler is not attached so the client can fall back
430
+ * to polling. Reuses duck-typing via `daemon.getScheduler()` — no new types
431
+ * exported from this module.
432
+ */
433
+ function handleSchedulesEventStream(req, res, daemon) {
434
+ const scheduler = daemon.getScheduler();
435
+ if (!scheduler) {
436
+ sendJson(res, 503, { error: 'Scheduler not attached' });
437
+ return;
438
+ }
439
+ res.writeHead(200, {
440
+ 'Content-Type': 'text/event-stream',
441
+ 'Cache-Control': 'no-cache, no-transform',
442
+ 'Connection': 'keep-alive',
443
+ 'X-Accel-Buffering': 'no',
444
+ });
445
+ // Initial frame so the client knows the channel is live even before
446
+ // the first scheduler event arrives (which may be minutes away).
447
+ res.write(`event: ready\ndata: ${JSON.stringify({ timestamp: Date.now() })}\n\n`);
448
+ let cleaned = false;
449
+ const cleanup = () => {
450
+ if (cleaned)
451
+ return;
452
+ cleaned = true;
453
+ clearInterval(heartbeat);
454
+ unsubscribe();
455
+ try {
456
+ res.end();
457
+ }
458
+ catch { /* already ended */ }
459
+ };
460
+ const unsubscribe = scheduler.on((event) => {
461
+ try {
462
+ res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
463
+ }
464
+ catch {
465
+ // Half-open socket: the request may never emit 'close'. Defer cleanup
466
+ // to the next microtask so we don't splice the listeners array we are
467
+ // currently being iterated from inside.
468
+ queueMicrotask(cleanup);
469
+ }
470
+ });
471
+ const heartbeat = setInterval(() => {
472
+ try {
473
+ res.write(`: ping ${Date.now()}\n\n`);
474
+ }
475
+ catch {
476
+ queueMicrotask(cleanup);
477
+ }
478
+ }, SSE_HEARTBEAT_MS);
479
+ if (typeof heartbeat.unref === 'function')
480
+ heartbeat.unref();
481
+ req.on('close', cleanup);
482
+ req.on('error', cleanup);
483
+ }
403
484
  // ============================================================================
404
485
  // Server lifecycle
405
486
  // ============================================================================
@@ -479,11 +560,28 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
479
560
  <head>
480
561
  <meta charset="utf-8">
481
562
  <meta name="viewport" content="width=device-width, initial-scale=1">
482
- <title>MoFlo Dashboard</title>
563
+ <title>The Arcane Console</title>
564
+ <meta name="description" content="The Arcane Console — moflo daemon, scheduled spells, and live event stream">
565
+ <meta property="og:title" content="The Arcane Console">
566
+ <meta property="og:description" content="The Arcane Console — moflo daemon, scheduled spells, and live event stream">
567
+ <link rel="preconnect" href="https://fonts.googleapis.com">
568
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
569
+ <link href="https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@700;900&display=swap" rel="stylesheet">
483
570
  <style>
484
571
  * { margin: 0; padding: 0; box-sizing: border-box; }
485
572
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0d1117; color: #c9d1d9; padding: 20px; }
486
- h1 { color: #58a6ff; margin-bottom: 4px; font-size: 1.5rem; }
573
+ /* Wizardy chain Cinzel Decorative is the Google Font; the rest are
574
+ the most likely system serifs across macOS / Windows / Linux so the
575
+ title still reads "arcane" if the user is offline or behind a font-CDN
576
+ block (Georgia ships everywhere; serif is the universal fallback). */
577
+ h1 { font-family: 'Cinzel Decorative', 'Cinzel', 'Trajan Pro', 'Palatino Linotype', 'Book Antiqua', Georgia, serif; font-weight: 900; letter-spacing: 0.04em; margin-bottom: 4px; font-size: 1.85rem; }
578
+ /* Per-word arcane palette. Hues chosen at L≈45-50%, S≈60-70% so they read on
579
+ both #0d1117 (dark) and #ffffff (light) — WCAG-AA at large-text size on
580
+ both. Each word's shadow matches its own hue so the glow doesn't bleed
581
+ a single color across all three. */
582
+ h1 .w-the { color: #8b5cf6; text-shadow: 0 0 18px rgba(139, 92, 246, 0.18); }
583
+ h1 .w-arcane { color: #2563eb; text-shadow: 0 0 18px rgba(37, 99, 235, 0.18); }
584
+ h1 .w-console { color: #059669; text-shadow: 0 0 18px rgba(5, 150, 105, 0.18); }
487
585
  h2 { color: #8b949e; font-size: 1.1rem; margin: 16px 0 12px; border-bottom: 1px solid #21262d; padding-bottom: 6px; }
488
586
  .header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 16px; }
489
587
  .subtitle { color: #8b949e; font-size: 0.85rem; }
@@ -531,13 +629,13 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
531
629
  </head>
532
630
  <body>
533
631
  <div class="header">
534
- <h1>MoFlo Dashboard</h1>
535
- <span class="subtitle">read-only &bull; localhost</span>
632
+ <h1><span class="w-the">The</span> <span class="w-arcane">Arcane</span> <span class="w-console">Console</span></h1>
633
+ <span class="subtitle">moflo daemon &bull; localhost</span>
536
634
  </div>
537
635
  <div id="status-bar" class="status-bar"><div class="empty">Loading...</div></div>
538
636
  <div class="nav" id="nav"></div>
539
637
  <div id="panel-workers" class="panel"></div>
540
- <div id="panel-schedules" class="panel" style="display:none"></div>
638
+ <div id="panel-schedules" class="panel" style="display:none"><div id="schedules-active"></div><div id="schedules-events"></div></div>
541
639
  <div id="panel-executions" class="panel" style="display:none"></div>
542
640
  <div id="panel-memory" class="panel" style="display:none"></div>
543
641
  <div id="poll-indicator" class="poll-indicator"></div>
@@ -613,15 +711,25 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
613
711
 
614
712
  function renderWorkers(s) {
615
713
  if (!s) return;
616
- const rows = s.workers.map(w =>
617
- '<tr><td>' + esc(w.type) + '</td>' +
618
- '<td>' + (w.isRunning ? badge('running','yellow') : badge('idle','gray')) + '</td>' +
619
- '<td>' + w.runCount + '</td>' +
620
- '<td>' + pct(w.successCount, w.failureCount) + '</td>' +
621
- '<td>' + fmtDuration(w.averageDurationMs) + '</td>' +
622
- '<td>' + fmtTimeAgo(w.lastRun) + '</td>' +
623
- '<td>' + (w.nextRun ? fmtTime(w.nextRun) : '-') + '</td></tr>'
624
- ).join('');
714
+ // Disabled workers show a clear "disabled" badge and dim "—" cells
715
+ // instead of "idle"/"never" those terms imply the worker is healthy
716
+ // but quiet, which misled users into thinking audit/predict/document
717
+ // were broken (#968).
718
+ const rows = s.workers.map(w => {
719
+ const statusBadge = w.enabled === false
720
+ ? badge('disabled', 'gray')
721
+ : (w.isRunning ? badge('running', 'yellow') : badge('idle', 'gray'));
722
+ const dim = '<span class="dim">—</span>';
723
+ const lastRun = w.enabled === false && !w.lastRun ? dim : fmtTimeAgo(w.lastRun);
724
+ const nextRun = w.enabled === false ? dim : (w.nextRun ? fmtTime(w.nextRun) : '-');
725
+ return '<tr><td>' + esc(w.type) + '</td>' +
726
+ '<td>' + statusBadge + '</td>' +
727
+ '<td>' + w.runCount + '</td>' +
728
+ '<td>' + pct(w.successCount, w.failureCount) + '</td>' +
729
+ '<td>' + fmtDuration(w.averageDurationMs) + '</td>' +
730
+ '<td>' + lastRun + '</td>' +
731
+ '<td>' + nextRun + '</td></tr>';
732
+ }).join('');
625
733
  document.getElementById('panel-workers').innerHTML =
626
734
  '<h2>Worker Status</h2>' +
627
735
  '<table><thead><tr><th>Worker</th><th>Status</th><th>Runs</th><th>Success</th><th>Avg</th><th>Last Run</th><th>Next Run</th></tr></thead>' +
@@ -643,7 +751,7 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
643
751
  window.__schedAction = scheduleAction;
644
752
 
645
753
  function renderSchedules(sc) {
646
- const el = document.getElementById('panel-schedules');
754
+ const el = document.getElementById('schedules-active');
647
755
  if (!sc) { el.innerHTML = '<div class="empty">Loading...</div>'; return; }
648
756
 
649
757
  if (sc.disabledInConfig) {
@@ -684,6 +792,67 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
684
792
  if (sc.history && sc.history.length) renderSchedulesHistory(el, sc.history, /*append*/ true);
685
793
  }
686
794
 
795
+ // Live events tail (Server-Sent Events from /api/schedules/events).
796
+ // The events div lives outside renderSchedules' write target so polled
797
+ // re-renders of the schedules panel don't touch it; pushSchedEvent is
798
+ // the only writer. Single source of truth for known event types + their
799
+ // badge color — adding a new schedule:* type means one entry here.
800
+ const SCHED_EVENT_BADGES = {
801
+ 'schedule:due': 'gray',
802
+ 'schedule:started': 'gray',
803
+ 'schedule:completed': 'green',
804
+ 'schedule:failed': 'red',
805
+ 'schedule:skipped': 'yellow',
806
+ 'schedule:disabled': 'yellow',
807
+ 'schedule:catchup': 'yellow',
808
+ };
809
+ const SCHED_EVENT_TYPES = Object.keys(SCHED_EVENT_BADGES);
810
+ const SCHED_EVENTS_MAX = 50;
811
+ const schedEvents = [];
812
+
813
+ function renderEventsTail() {
814
+ const el = document.getElementById('schedules-events');
815
+ if (!el) return;
816
+ if (schedEvents.length === 0) {
817
+ el.innerHTML = '<h2>Live Events</h2><div class="empty">Waiting for scheduler activity…</div>';
818
+ return;
819
+ }
820
+ const rows = schedEvents.map(e => {
821
+ const t = e.type || '?';
822
+ const short = String(t).replace('schedule:', '');
823
+ return '<tr><td>' + new Date(e.timestamp || Date.now()).toLocaleTimeString() + '</td>' +
824
+ '<td>' + badge(short, SCHED_EVENT_BADGES[t] || 'gray') + '</td>' +
825
+ '<td>' + esc(e.spellName || '-') + '</td>' +
826
+ '<td>' + esc(e.message || '') + '</td></tr>';
827
+ }).join('');
828
+ el.innerHTML = '<h2>Live Events</h2>' +
829
+ '<table><thead><tr><th>Time</th><th>Event</th><th>Spell</th><th>Detail</th></tr></thead>' +
830
+ '<tbody>' + rows + '</tbody></table>';
831
+ }
832
+ function pushSchedEvent(ev) {
833
+ schedEvents.unshift(ev);
834
+ if (schedEvents.length > SCHED_EVENTS_MAX) schedEvents.length = SCHED_EVENTS_MAX;
835
+ renderEventsTail();
836
+ }
837
+ renderEventsTail(); // Initial empty-state paint.
838
+
839
+ // Subscribe via EventSource. Browser handles auto-reconnect with backoff.
840
+ let evtSource = null;
841
+ function connectEventStream() {
842
+ try { if (evtSource) evtSource.close(); } catch (e) { /* ignore */ }
843
+ try {
844
+ evtSource = new EventSource('/api/schedules/events');
845
+ SCHED_EVENT_TYPES.forEach(t => {
846
+ evtSource.addEventListener(t, ev => {
847
+ try { pushSchedEvent(JSON.parse(ev.data)); } catch (e) { /* malformed frame */ }
848
+ });
849
+ });
850
+ } catch (e) {
851
+ console.warn('Event stream unavailable:', e);
852
+ }
853
+ }
854
+ connectEventStream();
855
+
687
856
  function renderSchedulesHistory(el, history, append) {
688
857
  const rows = history.map(h => {
689
858
  const statusBadge = h.success === true ? badge('pass','green')
@@ -1,29 +1,36 @@
1
1
  /**
2
2
  * Daemon Readiness Check for Scheduled Spells
3
3
  *
4
- * Lazy three-state flow: only triggered when creating schedules.
5
- * 1. Is daemon running? If not, prompt to start it.
6
- * 2. Is daemon installed as OS service? If not, prompt to install.
7
- * Always creates the schedule regardless the daemon picks it up on next start.
4
+ * Confirms the daemon is currently running so a freshly-created schedule
5
+ * actually fires. Prompts the user to start it interactively, or warns
6
+ * non-interactively. Always returns regardless of state the caller still
7
+ * writes the schedule, and the daemon picks it up on next start.
8
+ *
9
+ * OS-autostart install/uninstall is no longer handled here — see
10
+ * `daemon-autostart-lifecycle.ts`. That side effect is now driven by the
11
+ * count of enabled schedules, not a per-create prompt.
8
12
  */
9
13
  import { join, resolve } from 'path';
10
14
  import { mkdirSync, openSync, closeSync } from 'fs';
11
15
  import { spawn } from 'child_process';
12
16
  import { getDaemonLockHolder } from './daemon-lock.js';
13
- import { isDaemonInstalled, installDaemonService } from './daemon-service.js';
17
+ import { isDaemonInstalled } from './daemon-service.js';
14
18
  import { locateMofloCliBin } from './moflo-require.js';
15
19
  import { registerBackgroundPid } from './process-registry.js';
16
20
  /**
17
21
  * Ensure the daemon is ready for scheduled spell execution.
18
22
  *
19
- * Checks daemon state and prompts the user to start/install as needed.
20
- * Always returns — never throws. The caller should create the schedule
21
- * regardless of the result, since the daemon can pick it up later.
23
+ * Checks daemon state and prompts the user to start it as needed. Always
24
+ * returns — never throws. The caller should create the schedule regardless
25
+ * of the result, since the daemon can pick it up later. OS-autostart is
26
+ * reconciled separately by the create/cancel paths via
27
+ * `reconcileDaemonAutostart`.
22
28
  */
23
29
  export async function ensureDaemonForScheduling(options) {
24
30
  const resolvedRoot = resolve(options.projectRoot);
25
31
  const promptFn = options.promptConfirm ?? defaultPromptConfirm;
26
32
  const startFn = options.startDaemon ?? defaultStartDaemon;
33
+ const installedFn = options.isDaemonInstalledFn ?? isDaemonInstalled;
27
34
  const result = {
28
35
  daemonRunning: false,
29
36
  daemonInstalled: false,
@@ -50,29 +57,10 @@ export async function ensureDaemonForScheduling(options) {
50
57
  result.warnings.push('Daemon is not running. Start it with: moflo daemon start');
51
58
  }
52
59
  }
53
- // Step 2: Check if daemon is installed as OS autostart service. This
54
- // check is independent of the running state — a user with the daemon
55
- // currently down still needs the autostart warning so their new schedule
56
- // survives the next reboot.
57
- result.daemonInstalled = isDaemonInstalled(resolvedRoot);
58
- if (!result.daemonInstalled) {
59
- if (options.interactive && result.daemonRunning) {
60
- const shouldInstall = await promptFn('Register the daemon as a login service so schedules survive reboots?');
61
- if (shouldInstall) {
62
- const installResult = installDaemonService(resolvedRoot);
63
- result.daemonInstalled = installResult.success;
64
- if (!installResult.success) {
65
- result.warnings.push(`Failed to install daemon service: ${installResult.message}`);
66
- }
67
- }
68
- else {
69
- result.warnings.push("Daemon is not set to autostart. Run 'moflo daemon install' so this schedule survives reboot.");
70
- }
71
- }
72
- else {
73
- result.warnings.push("Daemon is not set to autostart. Run 'moflo daemon install' so this schedule survives reboot.");
74
- }
75
- }
60
+ // Surface OS install state purely informationally no prompts. The
61
+ // create/cancel commands reconcile install/uninstall via
62
+ // reconcileDaemonAutostart, driven by the enabled-schedule count.
63
+ result.daemonInstalled = installedFn(resolvedRoot);
76
64
  return result;
77
65
  }
78
66
  async function defaultPromptConfirm(message) {
@@ -1,49 +1,53 @@
1
1
  /**
2
- * Idempotent ephemeral-namespace purge for moflo's memory DB (`.moflo/moflo.db`).
2
+ * Idempotent session-start memory cleanup for moflo's memory DB
3
+ * (`.moflo/moflo.db`).
3
4
  *
4
- * Story #729 retired four namespaces from the persistent memory layer because
5
- * they store internal moflo run-tracking — not user knowledge — and embedding
6
- * them polluted the search index:
5
+ * Two passes run in a single sql.js open:
7
6
  *
8
- * - `hive-mind` (MCP broadcast traffic)
9
- * - `tasklist` (spell run records)
10
- * - `epic-state` (epic progress tracking)
11
- * - `test-bridge-fix` (single-row leftover from a one-off test)
7
+ * 1. **Hard-purge** namespaces in {@link PURGE_ON_SESSION_START_NAMESPACES} —
8
+ * `hive-mind`, `epic-state`, `test-bridge-fix`. These store internal
9
+ * run-tracking that does not need to survive a session restart. (#729)
12
10
  *
13
- * This service hard-deletes any rows in those namespaces left over from prior
14
- * moflo versions, then VACUUMs to reclaim disk. Future writes to these
15
- * namespaces still land in the DB — but skip embedding generation entirely
16
- * (see {@link EPHEMERAL_NAMESPACES} in `memory/bridge-embedder.ts`).
11
+ * 2. **Retention trim** the `tasklist` namespace down to the most recent
12
+ * {@link TASKLIST_RETENTION_CAP} rows. `tasklist` is the dashboard's
13
+ * "Flo Runs" tab data source (`daemon-dashboard.ts handleSpells`); the
14
+ * pre-#968 contract hard-purged it on every session start, leaving the tab
15
+ * permanently empty. Trim instead so users see recent history without
16
+ * unbounded growth.
17
+ *
18
+ * Both passes share the file open + final VACUUM + atomic write, so disk I/O
19
+ * is the same as before. Writes back to disk only when something changed.
17
20
  *
18
21
  * Lives in `services/` so it has no dependency on the CLI command machinery.
19
- * That lets `bin/session-start-launcher.mjs` dynamic-import it and run the
20
- * purge in foreground BEFORE long-lived sql.js consumers (MCP server, daemon)
21
- * open the DB — sql.js dumps the whole snapshot on every flush and would
22
+ * That lets `bin/session-start-launcher.mjs` dynamic-import it and run in
23
+ * foreground BEFORE long-lived sql.js consumers (MCP server, daemon) open
24
+ * the DB — sql.js dumps the whole snapshot on every flush and would
22
25
  * otherwise clobber our cleanup (see #727's clobber-hazard analysis).
23
26
  *
24
27
  * @module cli/services/ephemeral-namespace-purge
25
28
  */
26
29
  /* eslint-disable @typescript-eslint/no-explicit-any */
27
- import { EPHEMERAL_NAMESPACES } from '../memory/bridge-embedder.js';
30
+ import { PURGE_ON_SESSION_START_NAMESPACES, TASKLIST_RETENTION_CAP, } from '../memory/bridge-embedder.js';
28
31
  import { mofloImport } from './moflo-require.js';
29
32
  import { atomicWriteFileSync } from './atomic-file-write.js';
30
33
  import { memoryDbPath } from './moflo-paths.js';
31
34
  /**
32
- * Hard-delete every row whose namespace is in {@link EPHEMERAL_NAMESPACES}
33
- * and VACUUM. Returns `{ purged: 0 }` on the happy path: no DB, sql.js
34
- * unavailable, schema lacks `memory_entries`, or no ephemeral rows present.
35
- * Errors propagate to the caller (the launcher absorbs them so a failed
36
- * purge never blocks session start).
35
+ * Hard-delete rows in {@link PURGE_ON_SESSION_START_NAMESPACES} and trim the
36
+ * `tasklist` namespace to its retention cap, then VACUUM. Returns
37
+ * `{ purged: 0, trimmed: 0 }` on the happy path: no DB, sql.js unavailable,
38
+ * schema lacks `memory_entries`, or nothing to clean. Errors propagate to
39
+ * the caller (the launcher absorbs them so a failed purge never blocks
40
+ * session start).
37
41
  */
38
42
  export async function purgeEphemeralNamespaces(options = {}) {
39
43
  const fs = await import('fs');
40
44
  const path = await import('path');
41
45
  const dbPath = path.resolve(options.dbPath ?? memoryDbPath(process.cwd()));
42
46
  if (!fs.existsSync(dbPath))
43
- return { purged: 0 };
47
+ return { purged: 0, trimmed: 0 };
44
48
  const initSqlJs = (await mofloImport('sql.js'))?.default;
45
49
  if (!initSqlJs)
46
- return { purged: 0 };
50
+ return { purged: 0, trimmed: 0 };
47
51
  const SQL = await initSqlJs();
48
52
  const buffer = fs.readFileSync(dbPath);
49
53
  const db = new SQL.Database(buffer);
@@ -52,21 +56,45 @@ export async function purgeEphemeralNamespaces(options = {}) {
52
56
  // a no-op so we don't VACUUM unrelated SQLite files.
53
57
  const probe = db.exec(`SELECT name FROM sqlite_master WHERE type='table' AND name='memory_entries' LIMIT 1`);
54
58
  if (!probe[0]?.values?.[0])
55
- return { purged: 0 };
56
- const namespaces = Array.from(EPHEMERAL_NAMESPACES);
59
+ return { purged: 0, trimmed: 0 };
60
+ // Single COUNT pass to gate both DELETEs — a clean DB is the steady
61
+ // state and we don't want two no-op DELETEs (with their query-planner
62
+ // overhead) on every session start.
63
+ const namespaces = Array.from(PURGE_ON_SESSION_START_NAMESPACES);
64
+ const cap = options.tasklistRetentionCap ?? TASKLIST_RETENTION_CAP;
57
65
  const placeholders = namespaces.map(() => '?').join(', ');
58
- // Single-scan delete + rowsModified: skips a redundant COUNT pass on dirty
59
- // DBs and avoids the prepare/bind/step/free overhead on clean ones. VACUUM
60
- // (and the disk write) only run when something was actually deleted.
61
- db.run(`DELETE FROM memory_entries WHERE namespace IN (${placeholders})`, namespaces);
62
- const purged = db.getRowsModified?.() ?? 0;
63
- if (purged === 0)
64
- return { purged: 0 };
66
+ const countRows = db.exec(`SELECT
67
+ (SELECT COUNT(*) FROM memory_entries WHERE namespace IN (${placeholders})) AS purgeable,
68
+ (SELECT COUNT(*) FROM memory_entries WHERE namespace = 'tasklist') AS tasklistTotal`, namespaces);
69
+ const counts = countRows[0]?.values?.[0] ?? [0, 0];
70
+ const purgeable = Number(counts[0] ?? 0);
71
+ const tasklistTotal = Number(counts[1] ?? 0);
72
+ let purged = 0;
73
+ if (purgeable > 0) {
74
+ db.run(`DELETE FROM memory_entries WHERE namespace IN (${placeholders})`, namespaces);
75
+ purged = db.getRowsModified?.() ?? 0;
76
+ }
77
+ let trimmed = 0;
78
+ if (tasklistTotal > cap) {
79
+ // Keep the newest `cap` rows by created_at, falling back to `id DESC`
80
+ // for legacy rows that predate the created_at-not-null schema (#728-era).
81
+ db.run(`DELETE FROM memory_entries
82
+ WHERE namespace = 'tasklist'
83
+ AND id NOT IN (
84
+ SELECT id FROM memory_entries
85
+ WHERE namespace = 'tasklist'
86
+ ORDER BY created_at DESC, id DESC
87
+ LIMIT ?
88
+ )`, [cap]);
89
+ trimmed = db.getRowsModified?.() ?? 0;
90
+ }
91
+ if (purged === 0 && trimmed === 0)
92
+ return { purged: 0, trimmed: 0 };
65
93
  // VACUUM has to run outside any open transaction; sql.js auto-commits
66
94
  // each `db.run`, so this is safe to chain.
67
95
  db.run('VACUUM');
68
96
  atomicWriteFileSync(dbPath, db.export());
69
- return { purged };
97
+ return { purged, trimmed };
70
98
  }
71
99
  finally {
72
100
  db.close();
@@ -2,21 +2,14 @@
2
2
  * Headless Worker Executor
3
3
  * Enables workers to invoke Claude Code in headless mode with configurable sandbox profiles.
4
4
  *
5
- * ADR-020: Headless Worker Integration Architecture
5
+ * ADR-020: Headless Worker Integration Architecture (#970 dropped the
6
+ * `audit`/`document`/`predict` workers — those entries from the original
7
+ * ADR are superseded; the rest still applies).
6
8
  * - Integrates with CLAUDE_CODE_HEADLESS and CLAUDE_CODE_SANDBOX_MODE environment variables
7
9
  * - Provides process pool for concurrent execution
8
10
  * - Builds context from file glob patterns
9
11
  * - Supports prompt templates and output parsing
10
12
  * - Implements timeout and graceful error handling
11
- *
12
- * Key Features:
13
- * - Process pool with configurable maxConcurrent
14
- * - Context building from file glob patterns with caching
15
- * - Prompt template system with context injection
16
- * - Output parsing (text, json, markdown)
17
- * - Timeout handling with graceful termination
18
- * - Execution logging for debugging
19
- * - Event emission for monitoring
20
13
  */
21
14
  import { spawn, execSync } from 'child_process';
22
15
  import { EventEmitter } from 'events';
@@ -27,17 +20,14 @@ import { errorDetail } from '../shared/utils/error-detail.js';
27
20
  // Constants
28
21
  // ============================================
29
22
  /**
30
- * Array of headless worker types for runtime checking
23
+ * Array of headless worker types for runtime checking.
31
24
  */
32
25
  export const HEADLESS_WORKER_TYPES = [
33
- 'audit',
34
26
  'optimize',
35
27
  'testgaps',
36
- 'document',
37
28
  'ultralearn',
38
29
  'refactor',
39
30
  'deepdive',
40
- 'predict',
41
31
  ];
42
32
  /**
43
33
  * Array of local worker types
@@ -57,37 +47,11 @@ const MODEL_IDS = {
57
47
  haiku: 'claude-haiku-4-5-20251001',
58
48
  };
59
49
  /**
60
- * Default headless worker configurations based on ADR-020
50
+ * Default headless worker configurations based on ADR-020 (the
51
+ * `audit`/`document`/`predict` entries from the original ADR were dropped
52
+ * in #970 — see worker-daemon.ts header for rationale).
61
53
  */
62
54
  export const HEADLESS_WORKER_CONFIGS = {
63
- audit: {
64
- type: 'audit',
65
- mode: 'headless',
66
- intervalMs: 30 * 60 * 1000,
67
- priority: 'critical',
68
- description: 'AI-powered security analysis',
69
- enabled: true,
70
- headless: {
71
- promptTemplate: `Analyze this codebase for security vulnerabilities:
72
- - Check for hardcoded secrets (API keys, passwords)
73
- - Identify SQL injection risks
74
- - Find XSS vulnerabilities
75
- - Check for insecure dependencies
76
- - Identify authentication/authorization issues
77
-
78
- Provide a JSON report with:
79
- {
80
- "vulnerabilities": [{ "severity": "high|medium|low", "file": "...", "line": N, "description": "..." }],
81
- "riskScore": 0-100,
82
- "recommendations": ["..."]
83
- }`,
84
- sandbox: 'strict',
85
- model: 'haiku',
86
- outputFormat: 'json',
87
- contextPatterns: ['**/*.ts', '**/*.js', '**/.env*', '**/package.json'],
88
- timeoutMs: 5 * 60 * 1000,
89
- },
90
- },
91
55
  optimize: {
92
56
  type: 'optimize',
93
57
  mode: 'headless',
@@ -134,29 +98,6 @@ For each gap, provide a test skeleton.`,
134
98
  timeoutMs: 10 * 60 * 1000,
135
99
  },
136
100
  },
137
- document: {
138
- type: 'document',
139
- mode: 'headless',
140
- intervalMs: 120 * 60 * 1000,
141
- priority: 'low',
142
- description: 'AI documentation generation',
143
- enabled: false,
144
- headless: {
145
- promptTemplate: `Generate documentation for undocumented code:
146
- - Add JSDoc comments to functions
147
- - Create README sections for modules
148
- - Document API endpoints
149
- - Add inline comments for complex logic
150
- - Generate usage examples
151
-
152
- Focus on public APIs and exported functions.`,
153
- sandbox: 'permissive',
154
- model: 'haiku',
155
- outputFormat: 'markdown',
156
- contextPatterns: ['src/**/*.ts'],
157
- timeoutMs: 10 * 60 * 1000,
158
- },
159
- },
160
101
  ultralearn: {
161
102
  type: 'ultralearn',
162
103
  mode: 'headless',
@@ -232,34 +173,6 @@ Provide comprehensive report.`,
232
173
  timeoutMs: 15 * 60 * 1000,
233
174
  },
234
175
  },
235
- predict: {
236
- type: 'predict',
237
- mode: 'headless',
238
- intervalMs: 10 * 60 * 1000,
239
- priority: 'low',
240
- description: 'Predictive preloading',
241
- enabled: false,
242
- headless: {
243
- promptTemplate: `Based on recent activity, predict what the developer needs:
244
- - Files likely to be edited next
245
- - Tests that should be run
246
- - Documentation to reference
247
- - Dependencies to check
248
-
249
- Provide preload suggestions as JSON:
250
- {
251
- "filesToPreload": ["..."],
252
- "testsToRun": ["..."],
253
- "docsToReference": ["..."],
254
- "confidence": 0.0-1.0
255
- }`,
256
- sandbox: 'strict',
257
- model: 'haiku',
258
- outputFormat: 'json',
259
- contextPatterns: ['.moflo/metrics/*.json'],
260
- timeoutMs: 2 * 60 * 1000,
261
- },
262
- },
263
176
  };
264
177
  /**
265
178
  * Local worker configurations
@@ -79,6 +79,10 @@ export function getReferenceHookBlock() {
79
79
  { matcher: '^mcp__moflo__memory_(search|retrieve|list|stats|store)$', hooks: [gateHook('record-memory-searched', 3000)] },
80
80
  { matcher: '^TaskUpdate$', hooks: [gateCjs('check-task-transition', 2000)] },
81
81
  { matcher: '^mcp__moflo__memory_store$', hooks: [gateCjs('record-learnings-stored', 2000)] },
82
+ // #952 — wired so /fl -s/--swarm and /fl -h/--hive runs satisfy the
83
+ // check-before-agent gate after the protected MCP init has been called.
84
+ { matcher: '^mcp__moflo__swarm_init$', hooks: [gateCjs('record-swarm-init', 2000)] },
85
+ { matcher: '^mcp__moflo__hive-mind_init$', hooks: [gateCjs('record-hive-init', 2000)] },
82
86
  ],
83
87
  UserPromptSubmit: [
84
88
  { hooks: [helperHook('prompt-hook.mjs', '', 3000)] },