moflo 4.10.9 → 4.10.11

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.
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: luminarium
3
+ description: |
4
+ Print the localhost URL for The Luminarium — moflo's daemon dashboard — for the current project.
5
+ Use when the user asks for "the luminarium link", "the moflo dashboard", "the daemon UI", or anything synonymous.
6
+ Each project gets a deterministic port in 33000–33999; the actual bound port is recorded in `.moflo/daemon.lock`.
7
+ ---
8
+
9
+ # /luminarium — Project Dashboard Link
10
+
11
+ Surface the URL for The Luminarium (the moflo daemon's localhost UI) for the project that this session is running in. No prompts, no confirmations — print the link and stop.
12
+
13
+ ## Procedure
14
+
15
+ 1. **Find the project root.** Walk up from `process.cwd()` looking for a `.moflo/` directory. The session's cwd is almost always the project root, so check there first.
16
+
17
+ 2. **Read `.moflo/daemon.lock`.** It's a JSON file written by the daemon at bind time. The dashboard port is the `port` field:
18
+
19
+ ```json
20
+ { "pid": 12345, "port": 33421, "startedAt": "...", ... }
21
+ ```
22
+
23
+ - If the file exists and `port` is a valid number → the daemon is running and bound. Use that port.
24
+ - If the file is missing or `port` is absent/invalid → the daemon is not running. See step 4.
25
+
26
+ 3. **Print the link** in a single line, with the path verbatim — Claude Code renders it as clickable:
27
+
28
+ ```
29
+ The Luminarium: http://localhost:<port>
30
+ ```
31
+
32
+ Nothing else. No banner, no follow-up question, no "what would you like to do?".
33
+
34
+ 4. **If the daemon isn't running** (no lock file, or unparseable), say so in one line and offer the start command — don't run it:
35
+
36
+ ```
37
+ The moflo daemon isn't running for this project. Start it with: npx flo daemon start
38
+ ```
39
+
40
+ ## Why read the lock, not compute the port
41
+
42
+ The port is project-deterministic (sha256(projectRoot) mapped into 33000–33999), but if the deterministic port was already taken at bind time the daemon scans forward and binds an alternate. The lock file is the only source of truth for what's actually bound. Do not compute the hash yourself — read the file.
43
+
44
+ ## Don't
45
+
46
+ - Don't fall back to any hardcoded port — there is no project-agnostic dashboard port; a literal would route to a foreign daemon on a multi-project machine. If the lock is missing, report "not running".
47
+ - Don't compute the deterministic port and report it as the link when the lock is missing — the daemon may be down, or bound to an alternate port. Report "not running" instead.
48
+ - Don't run `flo daemon start` automatically — the user asked for a link, not for daemon management. Leave starting to `/healer` or the user.
49
+ - Don't open a browser. Print the URL; let the user click.
50
+
51
+ ## Output
52
+
53
+ A single line. Examples:
54
+
55
+ ```
56
+ The Luminarium: http://localhost:33421
57
+ ```
58
+
59
+ ```
60
+ The moflo daemon isn't running for this project. Start it with: npx flo daemon start
61
+ ```
62
+
63
+ ## See Also
64
+
65
+ - `/healer` — diagnoses and (with `--fix`) starts the daemon if it's not running.
66
+ - `src/cli/services/daemon-port.ts` (and its JS twin `bin/lib/daemon-port.mjs`) — canonical port-resolution helpers; `resolveClientPort()` is what the rest of moflo uses.
package/README.md CHANGED
@@ -419,7 +419,7 @@ flo daemon status # shows whether the service is registered AND running
419
419
 
420
420
  `flo spell schedule create` warns when the daemon isn't installed so you don't quietly miss runs.
421
421
 
422
- **Monitoring.** **The Luminarium** (the moflo daemon's localhost UI) surfaces live schedules, recent executions, and per-schedule controls (disable / re-enable / run now). It starts alongside the daemon at `http://localhost:3117` (override with `--dashboard-port` or disable with `--no-dashboard`).
422
+ **Monitoring.** **[The Luminarium](#the-luminarium)** moflo's localhost daemon dashboard — surfaces live schedules, recent executions, and per-schedule controls (disable / re-enable / run now), alongside worker health, memory stats, and Claude Code session stats. Each project gets its own deterministic port (33000–33999) recorded in `.moflo/daemon.lock`; ask `/luminarium` in your Claude session and it'll print the link.
423
423
 
424
424
  For full configuration (`scheduler:` block in `moflo.yaml`), event types, and the catch-up window after restarts, see [docs/SPELLS.md#scheduling](docs/SPELLS.md#scheduling).
425
425
 
@@ -459,6 +459,35 @@ flo epic reset 42 # Reset state for re-run
459
459
 
460
460
  See the [Epic handling](#epic-handling) section above for detection criteria and the comparison between `/flo <epic>` and `flo epic run`.
461
461
 
462
+ ## The Luminarium
463
+
464
+ The Luminarium is moflo's localhost daemon dashboard. It boots automatically with the background daemon (no extra service to install) and stays running as long as the daemon is up.
465
+
466
+ ### Finding the URL
467
+
468
+ Each project gets a deterministic port in the range 33000–33999, derived from a hash of the project root so two projects never collide on the same machine. The actual bound port is written to `.moflo/daemon.lock` when the daemon starts — if the deterministic port is already taken the daemon scans forward, so the lock file is the source of truth, not the hash.
469
+
470
+ Three ways to get the URL:
471
+
472
+ - **`/luminarium`** — inside a Claude Code session in a moflo project, this skill reads `.moflo/daemon.lock` and prints `http://localhost:<port>`. Fastest path.
473
+ - **`flo daemon status`** — prints the URL alongside the health summary.
474
+ - **`cat .moflo/daemon.lock`** — read the JSON directly: `{ "pid": ..., "port": 33421, ... }`.
475
+
476
+ ### What it shows
477
+
478
+ | Tab | What you see |
479
+ |-----|--------------|
480
+ | **Workers** | Live agent processes the daemon is running (statusline updater, indexer, embedder, etc.) |
481
+ | **Schedules** | All registered spell schedules (cron / interval / one-time), with run-now and disable controls |
482
+ | **Executions** | Recent spell runs — duration, exit code, step-by-step output |
483
+ | **Memory** | Memory namespace breakdown, vector count, embedder backend, HNSW index health |
484
+ | **Claude Stats** | Per-session Claude Code transcript stats — tokens, tools called, files touched (local primary sessions only) |
485
+
486
+ ### Flags
487
+
488
+ - `flo daemon start --no-dashboard` — disable the HTTP server entirely (the daemon itself still runs)
489
+ - `flo daemon start --dashboard-port <N>` — pin to a specific port, overriding the deterministic resolver. Also accepts the `MOFLO_DAEMON_PORT` env var, which the rest of moflo respects when talking to the daemon
490
+
462
491
  ## Commands
463
492
 
464
493
  You don't need to run these for normal use — `flo init` sets everything up, and the hooks handle memory, routing, and learning automatically. These commands are here for manual setup, debugging, and tweaking.
@@ -26,6 +26,7 @@ import { existsSync } from 'fs';
26
26
  import { errorDetail } from '../shared/utils/error-detail.js';
27
27
  import { memoryDbPath } from '../services/moflo-paths.js';
28
28
  import { findProjectRoot } from '../services/project-root.js';
29
+ import { purgeMemoryProbeNamespaces } from '../services/ephemeral-namespace-purge.js';
29
30
  import { loadToolArrays, getTool, pushDetail, summarizeFunctional, } from './doctor-checks-functional-shared.js';
30
31
  const MEMORY_ACCESS_CHECK = 'Memory Access Functional';
31
32
  const MEMORY_ACCESS_FAIL_FIX = 'Run `flo doctor --json` for per-subcheck details. Common fixes: ensure fastembed installed (memory_store.hasEmbedding=false), explicit threshold:0 honored (#837), or rebuild HNSW index (`flo memory rebuild-index`)';
@@ -563,6 +564,12 @@ export async function checkMemoryAccessFunctional() {
563
564
  }
564
565
  catch { /* ignore */ }
565
566
  }
567
+ // #1166 — namespace-level sweep backstop for the per-key safeDelete
568
+ // loop above (see purgeMemoryProbeNamespaces docstring for why).
569
+ try {
570
+ await purgeMemoryProbeNamespaces({ dbPath: memoryDbPath(findProjectRoot()) });
571
+ }
572
+ catch { /* best-effort */ }
566
573
  }
567
574
  return summarizeFunctional(MEMORY_ACCESS_CHECK, details, {
568
575
  passSuffix: '(memory_store + memory_search round-trip verified across subagent, swarm-agent, and hive-mind contexts)',
@@ -35,6 +35,7 @@ export const SKILLS_MAP = {
35
35
  'guidance',
36
36
  'healer',
37
37
  'flo-simplify',
38
+ 'luminarium',
38
39
  'reasoningbank-intelligence',
39
40
  ],
40
41
  memory: [
@@ -116,13 +116,16 @@ export const PURGE_ON_SESSION_START_NAMESPACES = new Set([
116
116
  * spawns a NEW namespace, so namespace pollution grows linearly with
117
117
  * healer-run count if cleanup races fail.
118
118
  *
119
- * Both probes register an explicit cleanup via `safeDelete`, but the
120
- * cleanup is best-effort and silently swallows failures (e.g. daemon
121
- * races, MCP transport errors) so rows accumulate across consumer
122
- * sessions. Auto-purging matches the pattern for
123
- * `hive-mind`/`epic-state`/`test-bridge-fix`. These rows MUST still get
124
- * embeddings (see {@link EPHEMERAL_NAMESPACE_PREFIXES} for why) only
125
- * their persistence across sessions is curtailed.
119
+ * Both probes register an explicit cleanup via `safeDelete`. Post-#1166
120
+ * the doctor also runs an in-process namespace sweep over these prefixes
121
+ * inside `checkMemoryAccessFunctional`'s finally block, so a healthy
122
+ * doctor run leaves zero rows behind. The session-start launcher's
123
+ * prefix-purge is now strictly a safety net for crashed-process residue
124
+ * (the doctor never reached its finally) or pre-#1166 consumer DBs that
125
+ * still carry accumulated probe rows. Auto-purging matches the pattern
126
+ * for `hive-mind`/`epic-state`/`test-bridge-fix`. These rows MUST still
127
+ * get embeddings (see {@link EPHEMERAL_NAMESPACE_PREFIXES} for why) —
128
+ * only their persistence across sessions is curtailed.
126
129
  */
127
130
  export const PURGE_ON_SESSION_START_PREFIXES = new Set([
128
131
  'doctor-memprobe-',
@@ -798,6 +798,14 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
798
798
  .btn-primary { background: #238636; border-color: #2ea043; color: #fff; }
799
799
  .btn-primary:hover { background: #2ea043; border-color: #3fb950; }
800
800
  .dim { color: #484f58; font-size: 0.75rem; font-style: italic; }
801
+ /* Loading state for tabs whose data is slow on first paint (currently
802
+ Claude Stats, which walks the user's transcript dir — can take 10–15s
803
+ on a long history). Pure-CSS spinner; no image, no framework. */
804
+ .loading-block { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; padding: 48px 16px; color: #8b949e; }
805
+ .loading-block .spinner { width: 28px; height: 28px; border: 3px solid #30363d; border-top-color: #58a6ff; border-radius: 50%; animation: lum-spin 0.85s linear infinite; }
806
+ .loading-block .msg { font-size: 0.9rem; color: #c9d1d9; }
807
+ .loading-block .hint { font-size: 0.8rem; color: #8b949e; font-style: italic; max-width: 480px; text-align: center; line-height: 1.5; }
808
+ @keyframes lum-spin { to { transform: rotate(360deg); } }
801
809
  </style>
802
810
  </head>
803
811
  <body>
@@ -811,7 +819,7 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
811
819
  <div id="panel-schedules" class="panel" style="display:none"><div id="schedules-active"></div><div id="schedules-events"></div></div>
812
820
  <div id="panel-executions" class="panel" style="display:none"></div>
813
821
  <div id="panel-memory" class="panel" style="display:none"></div>
814
- <div id="panel-claude-stats" class="panel" style="display:none"></div>
822
+ <div id="panel-claude-stats" class="panel" style="display:none"><div class="loading-block" role="status" aria-label="Loading Claude Code transcripts"><div class="spinner"></div><div class="msg">Reading Claude Code transcripts…</div><div class="hint">First load can take 10–15 seconds — moflo walks every session file in this project's transcript directory. Subsequent loads in this tab are much faster.</div></div></div>
815
823
  <div id="poll-indicator" class="poll-indicator"></div>
816
824
  <script>
817
825
  // Tab navigation — plain DOM, no framework
@@ -1139,7 +1147,19 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
1139
1147
  };
1140
1148
  function renderClaudeStats(cs) {
1141
1149
  const el = document.getElementById('panel-claude-stats');
1142
- if (!cs) { el.innerHTML = '<div class="empty">Loading...</div>'; return; }
1150
+ // cs is null on first paint AND on fetch error (Promise chain uses
1151
+ // .catch(() => null)). Render the spinner block on both so the user
1152
+ // sees motion during the 10–15s transcript walk and during a transient
1153
+ // network blip — better than a static "Loading..." that looks frozen.
1154
+ if (!cs) {
1155
+ el.innerHTML =
1156
+ '<div class="loading-block" role="status" aria-label="Loading Claude Code transcripts">' +
1157
+ '<div class="spinner"></div>' +
1158
+ '<div class="msg">Reading Claude Code transcripts…</div>' +
1159
+ '<div class="hint">First load can take 10–15 seconds — moflo walks every session file in this project\\'s transcript directory. Subsequent loads in this tab are much faster.</div>' +
1160
+ '</div>';
1161
+ return;
1162
+ }
1143
1163
 
1144
1164
  // Always-visible disclaimer banner — keeps the scope and limits in
1145
1165
  // view so the numbers aren't read as account-wide truth.
@@ -105,4 +105,51 @@ export async function purgeEphemeralNamespaces(options = {}) {
105
105
  db.close();
106
106
  }
107
107
  }
108
+ /**
109
+ * Hard-delete rows whose namespace matches one of
110
+ * {@link PURGE_ON_SESSION_START_PREFIXES} — currently `doctor-memprobe-*`
111
+ * and `doctor-neighbors-*`. Scoped down from {@link purgeEphemeralNamespaces}:
112
+ * no exact-namespace pass, no tasklist trim, no VACUUM. Returns
113
+ * `{ purged: 0 }` on a missing DB / missing `memory_entries` / clean state.
114
+ *
115
+ * Intended for the doctor's Memory Access functional check finally block
116
+ * (#1166). Only the doctor writes to these namespaces in production, so
117
+ * sweeping by prefix at the end of every healer run kills the
118
+ * `populated:ephemeral-purged` flake class — a per-key `safeDelete` that
119
+ * silently no-ops (row not visible at delete time, MCP transport error,
120
+ * `memory_delete` returning `success: true, deleted: false`) no longer
121
+ * leaks a row into the next assertion. The launcher's session-start
122
+ * purge stays in place as a defence-in-depth safety net for residue from
123
+ * crashed-process scenarios where the doctor never reached its finally.
124
+ *
125
+ * Errors propagate to the caller (the doctor absorbs them so a failed
126
+ * sweep never poisons the check return value).
127
+ */
128
+ export async function purgeMemoryProbeNamespaces(options = {}) {
129
+ const fs = await import('fs');
130
+ const path = await import('path');
131
+ const dbPath = path.resolve(options.dbPath ?? memoryDbPath(process.cwd()));
132
+ if (!fs.existsSync(dbPath))
133
+ return { purged: 0 };
134
+ const prefixes = Array.from(PURGE_ON_SESSION_START_PREFIXES);
135
+ if (prefixes.length === 0)
136
+ return { purged: 0 };
137
+ const db = openDaemonDatabase(dbPath);
138
+ try {
139
+ const probe = db.exec(`SELECT name FROM sqlite_master WHERE type='table' AND name='memory_entries' LIMIT 1`);
140
+ if (!probe[0]?.values?.[0])
141
+ return { purged: 0 };
142
+ const whereClause = prefixes.map(() => 'namespace LIKE ?').join(' OR ');
143
+ const bindings = prefixes.map((p) => `${p}%`);
144
+ const countRows = db.exec(`SELECT COUNT(*) FROM memory_entries WHERE ${whereClause}`, bindings);
145
+ const purgeable = Number(countRows[0]?.values?.[0]?.[0] ?? 0);
146
+ if (purgeable === 0)
147
+ return { purged: 0 };
148
+ db.run(`DELETE FROM memory_entries WHERE ${whereClause}`, bindings);
149
+ return { purged: db.getRowsModified?.() ?? 0 };
150
+ }
151
+ finally {
152
+ db.close();
153
+ }
154
+ }
108
155
  //# sourceMappingURL=ephemeral-namespace-purge.js.map
@@ -2,5 +2,5 @@
2
2
  * Auto-generated by build. Do not edit manually.
3
3
  * Source of truth: root package.json → scripts/sync-version.mjs
4
4
  */
5
- export const VERSION = '4.10.9';
5
+ export const VERSION = '4.10.11';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "moflo",
3
- "version": "4.10.9",
3
+ "version": "4.10.11",
4
4
  "description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
5
5
  "main": "dist/src/cli/index.js",
6
6
  "type": "module",
@@ -95,7 +95,7 @@
95
95
  "@typescript-eslint/eslint-plugin": "^7.18.0",
96
96
  "@typescript-eslint/parser": "^7.18.0",
97
97
  "eslint": "^8.0.0",
98
- "moflo": "^4.10.8",
98
+ "moflo": "^4.10.10",
99
99
  "tsx": "^4.21.0",
100
100
  "typescript": "^5.9.3",
101
101
  "vitest": "^4.0.0"